diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd9ef0e25..b706659d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
Sometimes the changelog has notes (not comprehensive) acknowledging people's work. This does not mean the changes are their fault, only that their code was used. All changes to the script are chosen by and the fault of the maintainer (ccd0).
+### v1.11.21
+
+**v1.11.21.0** *(2015-12-13)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.21.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.21.0/builds/4chan-X-noupdate.crx "Chromium version")]
+- Based on v1.11.20.3.
+- (human) Add `exclude:` option to filters to apply filter to all boards except the specified ones.
+- The dashed underlining on the matching quotelink within a post previewed from a backlink (or vice versa) has been extended to links in inlined posts.
+- Various minor quotelink-related bugfixes.
+
### v1.11.20
**v1.11.20.3** *(2015-12-11)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.20.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.20.3/builds/4chan-X-noupdate.crx "Chromium version")]
diff --git a/builds/4chan-X-beta.crx b/builds/4chan-X-beta.crx
index 2a8676d7e..129fc6c11 100644
Binary files a/builds/4chan-X-beta.crx and b/builds/4chan-X-beta.crx differ
diff --git a/builds/4chan-X-beta.meta.js b/builds/4chan-X-beta.meta.js
index 3ce5976d5..1b863f4fa 100644
--- a/builds/4chan-X-beta.meta.js
+++ b/builds/4chan-X-beta.meta.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X beta
-// @version 1.11.20.3
+// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
diff --git a/builds/4chan-X-beta.user.js b/builds/4chan-X-beta.user.js
index 022573284..eb4be6534 100644
--- a/builds/4chan-X-beta.user.js
+++ b/builds/4chan-X-beta.user.js
@@ -1,7 +1,7 @@
// Generated by CoffeeScript
// ==UserScript==
// @name 4chan X beta
-// @version 1.11.20.3
+// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@@ -433,7 +433,7 @@
doc = d.documentElement;
g = {
- VERSION: '1.11.20.3',
+ VERSION: '1.11.21.0',
NAMESPACE: '4chan X.',
boards: {}
};
@@ -1404,6 +1404,8 @@
this.board = board1;
this.ID = +root.id.slice(2);
this.fullID = this.board + "." + this.ID;
+ this.context = this;
+ root.dataset.fullID = this.fullID;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@@ -1543,7 +1545,8 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
- if (!(match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/))) {
+ match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/);
+ if (!(match || (this.isClone && quotelink.dataset.postID))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@@ -1696,8 +1699,10 @@
Clone = (function(superClass) {
extend(Clone, superClass);
+ Clone.prototype.isClone = true;
+
function Clone(origin1, context1, contractThumb) {
- var file, info, inline, inlined, k, key, len1, len2, len3, nodes, post, q, ref, ref1, ref2, ref3, ref4, root, u, val;
+ var file, info, inline, inlined, k, key, len1, len2, len3, len4, node, nodes, post, q, ref, ref1, ref2, ref3, ref4, ref5, root, u, v, val;
this.origin = origin1;
this.context = context1;
ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'];
@@ -1707,6 +1712,13 @@
}
nodes = this.origin.nodes;
root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true);
+ Clone.prefix || (Clone.prefix = 0);
+ ref1 = [root].concat(slice.call($$('[id]', root)));
+ for (q = 0, len2 = ref1.length; q < len2; q++) {
+ node = ref1[q];
+ node.id = Clone.prefix + node.id;
+ }
+ Clone.prefix++;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@@ -1729,14 +1741,14 @@
} else {
this.nodes.backlinks = info.getElementsByClassName('backlink');
}
- ref1 = $$('.inline', post);
- for (q = 0, len2 = ref1.length; q < len2; q++) {
- inline = ref1[q];
+ ref2 = $$('.inline', post);
+ for (u = 0, len3 = ref2.length; u < len3; u++) {
+ inline = ref2[u];
$.rm(inline);
}
- ref2 = $$('.inlined', post);
- for (u = 0, len3 = ref2.length; u < len3; u++) {
- inlined = ref2[u];
+ ref3 = $$('.inlined', post);
+ for (v = 0, len4 = ref3.length; v < len4; v++) {
+ inlined = ref3[v];
$.rmClass(inlined, 'inlined');
}
root.hidden = false;
@@ -1767,11 +1779,12 @@
this.nodes.date = $('.dateTime', info);
}
this.parseQuotes();
+ this.quotes = slice.call(this.origin.quotes);
if (this.origin.file) {
this.file = {};
- ref3 = this.origin.file;
- for (key in ref3) {
- val = ref3[key];
+ ref4 = this.origin.file;
+ for (key in ref4) {
+ val = ref4[key];
this.file[key] = val;
}
file = $('.file', post);
@@ -1783,7 +1796,7 @@
if (this.file.videoThumb) {
this.file.thumb.muted = true;
}
- if ((ref4 = this.file.thumb) != null ? ref4.dataset.src : void 0) {
+ if ((ref5 = this.file.thumb) != null ? ref5.dataset.src : void 0) {
this.file.thumb.src = this.file.thumb.dataset.src;
this.file.thumb.removeAttribute('data-src');
}
@@ -1794,7 +1807,6 @@
if (this.origin.isDead) {
this.isDead = true;
}
- this.isClone = true;
root.dataset.clone = this.origin.clones.push(this) - 1;
}
@@ -2295,13 +2307,13 @@
})();
Fetcher = (function() {
- function Fetcher(boardID1, threadID1, postID1, root1, context1) {
+ function Fetcher(boardID1, threadID1, postID1, root1, quoter1) {
var post;
this.boardID = boardID1;
this.threadID = threadID1;
this.postID = postID1;
this.root = root1;
- this.context = context1;
+ this.quoter = quoter1;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
@@ -2319,15 +2331,23 @@
}
Fetcher.prototype.insert = function(post) {
- var clone, nodes;
+ var boardID, clone, k, len1, nodes, postID, quote, ref, ref1;
if (!this.root.parentNode) {
return;
}
- clone = post.addClone(this.context, $.hasClass(this.root, 'dialog'));
+ clone = post.addClone(this.quoter.context, $.hasClass(this.root, 'dialog'));
Main.callbackNodes(Clone, [clone]);
nodes = clone.nodes;
$.rmAll(nodes.root);
$.add(nodes.root, nodes.post);
+ ref = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
+ for (k = 0, len1 = ref.length; k < len1; k++) {
+ quote = ref[k];
+ ref1 = Get.postDataFromLink(quote), boardID = ref1.boardID, postID = ref1.postID;
+ if (postID === this.quoter.ID && boardID === this.quoter.board.ID) {
+ $.addClass(quote, 'forwardlink');
+ }
+ }
$.rmAll(this.root);
$.add(this.root, nodes.root);
return $.event('PostsInserted');
@@ -3089,10 +3109,10 @@
history.replaceState({}, '');
}
hash = this.location.hash.slice(1);
- if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) {
+ if (!(/^\d*p\d+$/.test(hash) && (post = $.id(hash)))) {
return;
}
- if ((Get.postFromRoot(post)).isHidden) {
+ if (!post.getBoundingClientRect().height) {
return;
}
return $.queueTask(function() {
@@ -4547,15 +4567,12 @@
return Get.threadFromRoot($.x('ancestor::div[@class="thread"]', node));
},
postFromRoot: function(root) {
- var boardID, index, link, post, postID;
+ var index, post;
if (root == null) {
return null;
}
- link = $('.postNum > a[href*="#"]', root);
- boardID = link.pathname.split(/\/+/)[1];
- postID = link.hash.slice(2);
+ post = g.posts[root.dataset.fullID];
index = root.dataset.clone;
- post = g.posts[boardID + "." + postID];
if (index) {
return post.clones[index];
} else {
@@ -4565,9 +4582,6 @@
postFromNode: function(root) {
return Get.postFromRoot($.x('(ancestor::div[contains(@class,"postContainer")][1]|following::div[contains(@class,"postContainer")][1])', root));
},
- contextFromNode: function(node) {
- return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', node));
- },
postDataFromLink: function(link) {
var boardID, path, postID, ref, threadID;
if (link.hostname === 'boards.4chan.org') {
@@ -4996,8 +5010,8 @@
return $.set(this.id + ".position", this.style.cssText);
};
hoverstart = function(arg) {
- var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
- root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, asapTest = arg.asapTest, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
+ var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
+ root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
o = {
root: root,
el: el,
@@ -5013,12 +5027,13 @@
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
- $.asap(function() {
- return !el.parentNode || asapTest();
- }, function() {
+ o.hover(o.latestEvent);
+ new MutationObserver(function() {
if (el.parentNode) {
return o.hover(o.latestEvent);
}
+ }).observe(el, {
+ childList: true
});
$.on(root, endEvents, o.hoverend);
if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) {
@@ -5032,10 +5047,11 @@
};
return $.on(doc, 'mousemove', o.workaround);
};
+ hoverstart.padding = 25;
hover = function(e) {
var clientX, clientY, height, left, ref, right, style, threshold, top;
this.latestEvent = e;
- height = this.height || this.el.offsetHeight;
+ height = (this.height || this.el.offsetHeight) + hoverstart.padding;
clientX = e.clientX, clientY = e.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
@@ -5249,7 +5265,7 @@
Filter = {
filters: {},
init: function() {
- var boards, err, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, regexp, stub, top;
+ var boards, err, excludes, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, stub, top;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
@@ -5270,6 +5286,9 @@
filter = line.replace(regexp[0], '');
boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
boards = boards === 'global' ? null : boards.split(',');
+ if (boards === null) {
+ excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase().split(',') : void 0) || null;
+ }
if (key === 'uniqueID' || key === 'MD5') {
regexp = regexp[1];
} else {
@@ -5281,10 +5300,10 @@
continue;
}
}
- op = ((ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref3[1] : void 0) || 'yes';
+ op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
stub = (function() {
- var ref4;
- switch ((ref4 = filter.match(/stub:(yes|no)/)) != null ? ref4[1] : void 0) {
+ var ref5;
+ switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
case 'yes':
return true;
case 'no':
@@ -5294,11 +5313,11 @@
}
})();
if (hl = /highlight/.test(filter)) {
- hl = ((ref4 = filter.match(/highlight:([\w-]+)/)) != null ? ref4[1] : void 0) || 'filter-highlight';
- top = ((ref5 = filter.match(/top:(yes|no)/)) != null ? ref5[1] : void 0) || 'yes';
+ hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
+ top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
top = top === 'yes';
}
- this.filters[key].push(this.createFilter(regexp, boards, op, stub, hl, top));
+ this.filters[key].push(this.createFilter(regexp, boards, excludes, op, stub, hl, top));
}
if (!this.filters[key].length) {
delete this.filters[key];
@@ -5312,7 +5331,7 @@
cb: this.node
});
},
- createFilter: function(regexp, boards, op, stub, hl, top) {
+ createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
var settings, test;
test = typeof regexp === 'string' ? function(value) {
return regexp === value;
@@ -5329,6 +5348,9 @@
if (boards && indexOf.call(boards, boardID) < 0) {
return false;
}
+ if (excludes && indexOf.call(excludes, boardID) >= 0) {
+ return false;
+ }
if (isReply && op === 'only' || !isReply && op === 'no') {
return false;
}
@@ -6259,7 +6281,7 @@
if (this.isClone && this.thread === this.context.thread) {
return;
}
- ref = this.isClone ? this.context : this, board = ref.board, thread = ref.thread;
+ ref = this.context, board = ref.board, thread = ref.thread;
ref1 = this.nodes.quotelinks;
for (k = 0, len1 = ref1.length; k < len1; k++) {
quotelink = ref1[k];
@@ -6330,20 +6352,21 @@
});
},
toggle: function(e) {
- var boardID, context, postID, ref, threadID;
+ var boardID, context, postID, quoter, ref, threadID;
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
return;
}
e.preventDefault();
ref = Get.postDataFromLink(this), boardID = ref.boardID, threadID = ref.threadID, postID = ref.postID;
- context = Get.contextFromNode(this);
+ quoter = Get.postFromNode(this);
+ context = quoter.context;
if ($.hasClass(this, 'inlined')) {
QuoteInline.rm(this, boardID, threadID, postID, context);
} else {
- if ($.x("ancestor::div[@id='pc" + postID + "']", this)) {
+ if ($.x("ancestor::div[@data-full-i-d='" + boardID + "." + postID + "']", this)) {
return;
}
- QuoteInline.add(this, boardID, threadID, postID, context);
+ QuoteInline.add(this, boardID, threadID, postID, context, quoter);
}
return this.classList.toggle('inlined');
},
@@ -6354,18 +6377,17 @@
return $.x('ancestor-or-self::*[parent::blockquote][1]', quotelink);
}
},
- add: function(quotelink, boardID, threadID, postID, context) {
+ add: function(quotelink, boardID, threadID, postID, context, quoter) {
var inline, isBacklink, post, qroot, root;
isBacklink = $.hasClass(quotelink, 'backlink');
inline = $.el('div', {
- id: "i" + postID,
className: 'inline'
});
root = QuoteInline.findRoot(quotelink, isBacklink);
$.after(root, inline);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.addClass(qroot, 'hasInline');
- new Fetcher(boardID, threadID, postID, inline, context);
+ new Fetcher(boardID, threadID, postID, inline, quoter);
if (!((post = g.posts[boardID + "." + postID]) && context.thread === post.thread)) {
return;
}
@@ -6382,7 +6404,7 @@
var el, inlined, isBacklink, post, qroot, ref, root;
isBacklink = $.hasClass(quotelink, 'backlink');
root = QuoteInline.findRoot(quotelink, isBacklink);
- root = $.x("following-sibling::div[@id='i" + postID + "'][1]", root);
+ root = $.x("following-sibling::div[div/@data-full-i-d='" + boardID + "." + postID + "'][1]", root);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.rm(root);
if (!$('.inline', qroot)) {
@@ -6435,7 +6457,7 @@
quotelink.textContent = quotelink.textContent.replace(QuoteOP.text, '');
}
}
- fullID = (this.isClone ? this.context : this).thread.fullID;
+ fullID = this.context.thread.fullID;
if (indexOf.call(quotes, fullID) < 0) {
return;
}
@@ -6472,7 +6494,7 @@
}
},
mouseover: function(e) {
- var boardID, clone, k, len1, len2, origin, post, postID, posts, q, qp, quote, quoterID, ref, ref1, threadID;
+ var boardID, k, len1, origin, post, postID, posts, qp, ref, threadID;
if ($.hasClass(this, 'inlined') || !d.contains(this)) {
return;
}
@@ -6482,21 +6504,15 @@
className: 'dialog'
});
$.add(Header.hover, qp);
- new Fetcher(boardID, threadID, postID, qp, Get.contextFromNode(this));
+ new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this));
UI.hover({
root: this,
el: qp,
latestEvent: e,
endEvents: 'mouseout click',
- cb: QuotePreview.mouseout,
- asapTest: function() {
- return qp.firstElementChild;
- }
+ cb: QuotePreview.mouseout
});
- if (!(origin = g.posts[boardID + "." + postID])) {
- return;
- }
- if (Conf['Quote Highlighting']) {
+ if (Conf['Quote Highlighting'] && (origin = g.posts[boardID + "." + postID])) {
posts = [origin].concat(origin.clones);
posts.pop();
for (k = 0, len1 = posts.length; k < len1; k++) {
@@ -6504,15 +6520,6 @@
$.addClass(post.nodes.post, 'qphl');
}
}
- quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0];
- clone = Get.postFromRoot(qp.firstChild);
- ref1 = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
- for (q = 0, len2 = ref1.length; q < len2; q++) {
- quote = ref1[q];
- if (quote.hash.slice(2) === quoterID) {
- $.addClass(quote, 'forwardlink');
- }
- }
},
mouseout: function() {
var clone, k, len1, post, ref, root;
@@ -6802,7 +6809,7 @@
if (highlight = $('.highlight')) {
$.rmClass(highlight, 'highlight');
}
- if (!QuoteYou.lastRead) {
+ if (!(QuoteYou.lastRead && doc.contains(QuoteYou.lastRead) && $.hasClass(QuoteYou.lastRead, 'quotesYou'))) {
if (!(post = QuoteYou.lastRead = $('.quotesYou'))) {
new Notice('warning', 'No posts are currently quoting you, loser.', 20);
return;
@@ -6814,7 +6821,7 @@
post = QuoteYou.lastRead;
}
str = type + "::div[contains(@class,'quotesYou')]";
- while (post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) {
+ while ((post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0))) {
if (QuoteYou.cb.scroll(post)) {
return;
}
@@ -6822,14 +6829,16 @@
posts = $$('.quotesYou');
return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]);
},
- scroll: function(post) {
- if (Get.postFromRoot(post).isHidden) {
+ scroll: function(root) {
+ var post;
+ post = $('.post', root);
+ if (!post.getBoundingClientRect().height) {
return false;
} else {
- QuoteYou.lastRead = post;
+ QuoteYou.lastRead = root;
window.location = "#" + post.id;
Header.scrollTo(post);
- $.addClass($('.post', post), 'highlight');
+ $.addClass(post, 'highlight');
return true;
}
}
@@ -6852,16 +6861,13 @@
},
node: function() {
var deadlink, k, len1, ref;
+ if (this.isClone) {
+ return;
+ }
ref = $$('.deadlink', this.nodes.comment);
for (k = 0, len1 = ref.length; k < len1; k++) {
deadlink = ref[k];
- if (this.isClone) {
- if ($.hasClass(deadlink, 'quotelink')) {
- this.nodes.quotelinks.push(deadlink);
- }
- } else {
- Quotify.parseDeadlink.call(this, deadlink);
- }
+ Quotify.parseDeadlink.call(this, deadlink);
}
},
parseDeadlink: function(deadlink) {
@@ -11107,7 +11113,7 @@
},
mouseover: function(post) {
return function(e) {
- var el, error, file, height, isVideo, left, maxHeight, maxWidth, padding, ref, ref1, ref2, right, scale, width, x;
+ var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
if (!doc.contains(this)) {
return;
}
@@ -11151,9 +11157,8 @@
return results;
})(), width = ref1[0], height = ref1[1];
ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
- padding = 25;
maxWidth = Math.max(left, doc.clientWidth - right);
- maxHeight = doc.clientHeight - padding;
+ maxHeight = doc.clientHeight - UI.hover.padding;
scale = Math.min(1, maxWidth / width, maxHeight / height);
el.style.maxWidth = (scale * width) + "px";
el.style.maxHeight = (scale * height) + "px";
@@ -11162,10 +11167,7 @@
el: el,
latestEvent: e,
endEvents: 'mouseout click',
- asapTest: function() {
- return true;
- },
- height: scale * height + padding,
+ height: scale * height,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@@ -17286,7 +17288,7 @@
return;
}
$.extend(div, {
- innerHTML: "
Filter is disabled.
Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:- Per boards, separate them with commas. It is global if not specified.
For example: boards:a,jp;. - Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).
For example: op:only;, op:no; or op:yes;. - Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).
For example: stub:yes; or stub:no;. - Highlight instead of hiding. You can specify a class name to use with a userstyle.
For example: highlight; or highlight:wallpaper;. - Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.
"
+ innerHTML: "Filter is disabled.
Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:- Per boards, separate them with commas. It is global if not specified.
For example: boards:a,jp;. - In case of a global rule, select boards to be excluded from the filter.
For example: exclude:vg,v;. - Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).
For example: op:only;, op:no; or op:yes;. - Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).
For example: stub:yes; or stub:no;. - Highlight instead of hiding. You can specify a class name to use with a userstyle.
For example: highlight; or highlight:wallpaper;. - Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.
"
});
return $('.warning', div).hidden = Conf['Filter'];
},
diff --git a/builds/4chan-X-noupdate.crx b/builds/4chan-X-noupdate.crx
index 7b9fe9603..3487328d5 100644
Binary files a/builds/4chan-X-noupdate.crx and b/builds/4chan-X-noupdate.crx differ
diff --git a/builds/4chan-X-noupdate.user.js b/builds/4chan-X-noupdate.user.js
index 886320449..58e5eaf6a 100644
--- a/builds/4chan-X-noupdate.user.js
+++ b/builds/4chan-X-noupdate.user.js
@@ -1,7 +1,7 @@
// Generated by CoffeeScript
// ==UserScript==
// @name 4chan X
-// @version 1.11.20.3
+// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@@ -433,7 +433,7 @@
doc = d.documentElement;
g = {
- VERSION: '1.11.20.3',
+ VERSION: '1.11.21.0',
NAMESPACE: '4chan X.',
boards: {}
};
@@ -1404,6 +1404,8 @@
this.board = board1;
this.ID = +root.id.slice(2);
this.fullID = this.board + "." + this.ID;
+ this.context = this;
+ root.dataset.fullID = this.fullID;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@@ -1543,7 +1545,8 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
- if (!(match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/))) {
+ match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/);
+ if (!(match || (this.isClone && quotelink.dataset.postID))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@@ -1696,8 +1699,10 @@
Clone = (function(superClass) {
extend(Clone, superClass);
+ Clone.prototype.isClone = true;
+
function Clone(origin1, context1, contractThumb) {
- var file, info, inline, inlined, k, key, len1, len2, len3, nodes, post, q, ref, ref1, ref2, ref3, ref4, root, u, val;
+ var file, info, inline, inlined, k, key, len1, len2, len3, len4, node, nodes, post, q, ref, ref1, ref2, ref3, ref4, ref5, root, u, v, val;
this.origin = origin1;
this.context = context1;
ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'];
@@ -1707,6 +1712,13 @@
}
nodes = this.origin.nodes;
root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true);
+ Clone.prefix || (Clone.prefix = 0);
+ ref1 = [root].concat(slice.call($$('[id]', root)));
+ for (q = 0, len2 = ref1.length; q < len2; q++) {
+ node = ref1[q];
+ node.id = Clone.prefix + node.id;
+ }
+ Clone.prefix++;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@@ -1729,14 +1741,14 @@
} else {
this.nodes.backlinks = info.getElementsByClassName('backlink');
}
- ref1 = $$('.inline', post);
- for (q = 0, len2 = ref1.length; q < len2; q++) {
- inline = ref1[q];
+ ref2 = $$('.inline', post);
+ for (u = 0, len3 = ref2.length; u < len3; u++) {
+ inline = ref2[u];
$.rm(inline);
}
- ref2 = $$('.inlined', post);
- for (u = 0, len3 = ref2.length; u < len3; u++) {
- inlined = ref2[u];
+ ref3 = $$('.inlined', post);
+ for (v = 0, len4 = ref3.length; v < len4; v++) {
+ inlined = ref3[v];
$.rmClass(inlined, 'inlined');
}
root.hidden = false;
@@ -1767,11 +1779,12 @@
this.nodes.date = $('.dateTime', info);
}
this.parseQuotes();
+ this.quotes = slice.call(this.origin.quotes);
if (this.origin.file) {
this.file = {};
- ref3 = this.origin.file;
- for (key in ref3) {
- val = ref3[key];
+ ref4 = this.origin.file;
+ for (key in ref4) {
+ val = ref4[key];
this.file[key] = val;
}
file = $('.file', post);
@@ -1783,7 +1796,7 @@
if (this.file.videoThumb) {
this.file.thumb.muted = true;
}
- if ((ref4 = this.file.thumb) != null ? ref4.dataset.src : void 0) {
+ if ((ref5 = this.file.thumb) != null ? ref5.dataset.src : void 0) {
this.file.thumb.src = this.file.thumb.dataset.src;
this.file.thumb.removeAttribute('data-src');
}
@@ -1794,7 +1807,6 @@
if (this.origin.isDead) {
this.isDead = true;
}
- this.isClone = true;
root.dataset.clone = this.origin.clones.push(this) - 1;
}
@@ -2295,13 +2307,13 @@
})();
Fetcher = (function() {
- function Fetcher(boardID1, threadID1, postID1, root1, context1) {
+ function Fetcher(boardID1, threadID1, postID1, root1, quoter1) {
var post;
this.boardID = boardID1;
this.threadID = threadID1;
this.postID = postID1;
this.root = root1;
- this.context = context1;
+ this.quoter = quoter1;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
@@ -2319,15 +2331,23 @@
}
Fetcher.prototype.insert = function(post) {
- var clone, nodes;
+ var boardID, clone, k, len1, nodes, postID, quote, ref, ref1;
if (!this.root.parentNode) {
return;
}
- clone = post.addClone(this.context, $.hasClass(this.root, 'dialog'));
+ clone = post.addClone(this.quoter.context, $.hasClass(this.root, 'dialog'));
Main.callbackNodes(Clone, [clone]);
nodes = clone.nodes;
$.rmAll(nodes.root);
$.add(nodes.root, nodes.post);
+ ref = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
+ for (k = 0, len1 = ref.length; k < len1; k++) {
+ quote = ref[k];
+ ref1 = Get.postDataFromLink(quote), boardID = ref1.boardID, postID = ref1.postID;
+ if (postID === this.quoter.ID && boardID === this.quoter.board.ID) {
+ $.addClass(quote, 'forwardlink');
+ }
+ }
$.rmAll(this.root);
$.add(this.root, nodes.root);
return $.event('PostsInserted');
@@ -3089,10 +3109,10 @@
history.replaceState({}, '');
}
hash = this.location.hash.slice(1);
- if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) {
+ if (!(/^\d*p\d+$/.test(hash) && (post = $.id(hash)))) {
return;
}
- if ((Get.postFromRoot(post)).isHidden) {
+ if (!post.getBoundingClientRect().height) {
return;
}
return $.queueTask(function() {
@@ -4547,15 +4567,12 @@
return Get.threadFromRoot($.x('ancestor::div[@class="thread"]', node));
},
postFromRoot: function(root) {
- var boardID, index, link, post, postID;
+ var index, post;
if (root == null) {
return null;
}
- link = $('.postNum > a[href*="#"]', root);
- boardID = link.pathname.split(/\/+/)[1];
- postID = link.hash.slice(2);
+ post = g.posts[root.dataset.fullID];
index = root.dataset.clone;
- post = g.posts[boardID + "." + postID];
if (index) {
return post.clones[index];
} else {
@@ -4565,9 +4582,6 @@
postFromNode: function(root) {
return Get.postFromRoot($.x('(ancestor::div[contains(@class,"postContainer")][1]|following::div[contains(@class,"postContainer")][1])', root));
},
- contextFromNode: function(node) {
- return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', node));
- },
postDataFromLink: function(link) {
var boardID, path, postID, ref, threadID;
if (link.hostname === 'boards.4chan.org') {
@@ -4996,8 +5010,8 @@
return $.set(this.id + ".position", this.style.cssText);
};
hoverstart = function(arg) {
- var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
- root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, asapTest = arg.asapTest, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
+ var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
+ root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
o = {
root: root,
el: el,
@@ -5013,12 +5027,13 @@
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
- $.asap(function() {
- return !el.parentNode || asapTest();
- }, function() {
+ o.hover(o.latestEvent);
+ new MutationObserver(function() {
if (el.parentNode) {
return o.hover(o.latestEvent);
}
+ }).observe(el, {
+ childList: true
});
$.on(root, endEvents, o.hoverend);
if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) {
@@ -5032,10 +5047,11 @@
};
return $.on(doc, 'mousemove', o.workaround);
};
+ hoverstart.padding = 25;
hover = function(e) {
var clientX, clientY, height, left, ref, right, style, threshold, top;
this.latestEvent = e;
- height = this.height || this.el.offsetHeight;
+ height = (this.height || this.el.offsetHeight) + hoverstart.padding;
clientX = e.clientX, clientY = e.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
@@ -5249,7 +5265,7 @@
Filter = {
filters: {},
init: function() {
- var boards, err, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, regexp, stub, top;
+ var boards, err, excludes, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, stub, top;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
@@ -5270,6 +5286,9 @@
filter = line.replace(regexp[0], '');
boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
boards = boards === 'global' ? null : boards.split(',');
+ if (boards === null) {
+ excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase().split(',') : void 0) || null;
+ }
if (key === 'uniqueID' || key === 'MD5') {
regexp = regexp[1];
} else {
@@ -5281,10 +5300,10 @@
continue;
}
}
- op = ((ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref3[1] : void 0) || 'yes';
+ op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
stub = (function() {
- var ref4;
- switch ((ref4 = filter.match(/stub:(yes|no)/)) != null ? ref4[1] : void 0) {
+ var ref5;
+ switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
case 'yes':
return true;
case 'no':
@@ -5294,11 +5313,11 @@
}
})();
if (hl = /highlight/.test(filter)) {
- hl = ((ref4 = filter.match(/highlight:([\w-]+)/)) != null ? ref4[1] : void 0) || 'filter-highlight';
- top = ((ref5 = filter.match(/top:(yes|no)/)) != null ? ref5[1] : void 0) || 'yes';
+ hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
+ top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
top = top === 'yes';
}
- this.filters[key].push(this.createFilter(regexp, boards, op, stub, hl, top));
+ this.filters[key].push(this.createFilter(regexp, boards, excludes, op, stub, hl, top));
}
if (!this.filters[key].length) {
delete this.filters[key];
@@ -5312,7 +5331,7 @@
cb: this.node
});
},
- createFilter: function(regexp, boards, op, stub, hl, top) {
+ createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
var settings, test;
test = typeof regexp === 'string' ? function(value) {
return regexp === value;
@@ -5329,6 +5348,9 @@
if (boards && indexOf.call(boards, boardID) < 0) {
return false;
}
+ if (excludes && indexOf.call(excludes, boardID) >= 0) {
+ return false;
+ }
if (isReply && op === 'only' || !isReply && op === 'no') {
return false;
}
@@ -6259,7 +6281,7 @@
if (this.isClone && this.thread === this.context.thread) {
return;
}
- ref = this.isClone ? this.context : this, board = ref.board, thread = ref.thread;
+ ref = this.context, board = ref.board, thread = ref.thread;
ref1 = this.nodes.quotelinks;
for (k = 0, len1 = ref1.length; k < len1; k++) {
quotelink = ref1[k];
@@ -6330,20 +6352,21 @@
});
},
toggle: function(e) {
- var boardID, context, postID, ref, threadID;
+ var boardID, context, postID, quoter, ref, threadID;
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
return;
}
e.preventDefault();
ref = Get.postDataFromLink(this), boardID = ref.boardID, threadID = ref.threadID, postID = ref.postID;
- context = Get.contextFromNode(this);
+ quoter = Get.postFromNode(this);
+ context = quoter.context;
if ($.hasClass(this, 'inlined')) {
QuoteInline.rm(this, boardID, threadID, postID, context);
} else {
- if ($.x("ancestor::div[@id='pc" + postID + "']", this)) {
+ if ($.x("ancestor::div[@data-full-i-d='" + boardID + "." + postID + "']", this)) {
return;
}
- QuoteInline.add(this, boardID, threadID, postID, context);
+ QuoteInline.add(this, boardID, threadID, postID, context, quoter);
}
return this.classList.toggle('inlined');
},
@@ -6354,18 +6377,17 @@
return $.x('ancestor-or-self::*[parent::blockquote][1]', quotelink);
}
},
- add: function(quotelink, boardID, threadID, postID, context) {
+ add: function(quotelink, boardID, threadID, postID, context, quoter) {
var inline, isBacklink, post, qroot, root;
isBacklink = $.hasClass(quotelink, 'backlink');
inline = $.el('div', {
- id: "i" + postID,
className: 'inline'
});
root = QuoteInline.findRoot(quotelink, isBacklink);
$.after(root, inline);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.addClass(qroot, 'hasInline');
- new Fetcher(boardID, threadID, postID, inline, context);
+ new Fetcher(boardID, threadID, postID, inline, quoter);
if (!((post = g.posts[boardID + "." + postID]) && context.thread === post.thread)) {
return;
}
@@ -6382,7 +6404,7 @@
var el, inlined, isBacklink, post, qroot, ref, root;
isBacklink = $.hasClass(quotelink, 'backlink');
root = QuoteInline.findRoot(quotelink, isBacklink);
- root = $.x("following-sibling::div[@id='i" + postID + "'][1]", root);
+ root = $.x("following-sibling::div[div/@data-full-i-d='" + boardID + "." + postID + "'][1]", root);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.rm(root);
if (!$('.inline', qroot)) {
@@ -6435,7 +6457,7 @@
quotelink.textContent = quotelink.textContent.replace(QuoteOP.text, '');
}
}
- fullID = (this.isClone ? this.context : this).thread.fullID;
+ fullID = this.context.thread.fullID;
if (indexOf.call(quotes, fullID) < 0) {
return;
}
@@ -6472,7 +6494,7 @@
}
},
mouseover: function(e) {
- var boardID, clone, k, len1, len2, origin, post, postID, posts, q, qp, quote, quoterID, ref, ref1, threadID;
+ var boardID, k, len1, origin, post, postID, posts, qp, ref, threadID;
if ($.hasClass(this, 'inlined') || !d.contains(this)) {
return;
}
@@ -6482,21 +6504,15 @@
className: 'dialog'
});
$.add(Header.hover, qp);
- new Fetcher(boardID, threadID, postID, qp, Get.contextFromNode(this));
+ new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this));
UI.hover({
root: this,
el: qp,
latestEvent: e,
endEvents: 'mouseout click',
- cb: QuotePreview.mouseout,
- asapTest: function() {
- return qp.firstElementChild;
- }
+ cb: QuotePreview.mouseout
});
- if (!(origin = g.posts[boardID + "." + postID])) {
- return;
- }
- if (Conf['Quote Highlighting']) {
+ if (Conf['Quote Highlighting'] && (origin = g.posts[boardID + "." + postID])) {
posts = [origin].concat(origin.clones);
posts.pop();
for (k = 0, len1 = posts.length; k < len1; k++) {
@@ -6504,15 +6520,6 @@
$.addClass(post.nodes.post, 'qphl');
}
}
- quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0];
- clone = Get.postFromRoot(qp.firstChild);
- ref1 = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
- for (q = 0, len2 = ref1.length; q < len2; q++) {
- quote = ref1[q];
- if (quote.hash.slice(2) === quoterID) {
- $.addClass(quote, 'forwardlink');
- }
- }
},
mouseout: function() {
var clone, k, len1, post, ref, root;
@@ -6802,7 +6809,7 @@
if (highlight = $('.highlight')) {
$.rmClass(highlight, 'highlight');
}
- if (!QuoteYou.lastRead) {
+ if (!(QuoteYou.lastRead && doc.contains(QuoteYou.lastRead) && $.hasClass(QuoteYou.lastRead, 'quotesYou'))) {
if (!(post = QuoteYou.lastRead = $('.quotesYou'))) {
new Notice('warning', 'No posts are currently quoting you, loser.', 20);
return;
@@ -6814,7 +6821,7 @@
post = QuoteYou.lastRead;
}
str = type + "::div[contains(@class,'quotesYou')]";
- while (post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) {
+ while ((post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0))) {
if (QuoteYou.cb.scroll(post)) {
return;
}
@@ -6822,14 +6829,16 @@
posts = $$('.quotesYou');
return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]);
},
- scroll: function(post) {
- if (Get.postFromRoot(post).isHidden) {
+ scroll: function(root) {
+ var post;
+ post = $('.post', root);
+ if (!post.getBoundingClientRect().height) {
return false;
} else {
- QuoteYou.lastRead = post;
+ QuoteYou.lastRead = root;
window.location = "#" + post.id;
Header.scrollTo(post);
- $.addClass($('.post', post), 'highlight');
+ $.addClass(post, 'highlight');
return true;
}
}
@@ -6852,16 +6861,13 @@
},
node: function() {
var deadlink, k, len1, ref;
+ if (this.isClone) {
+ return;
+ }
ref = $$('.deadlink', this.nodes.comment);
for (k = 0, len1 = ref.length; k < len1; k++) {
deadlink = ref[k];
- if (this.isClone) {
- if ($.hasClass(deadlink, 'quotelink')) {
- this.nodes.quotelinks.push(deadlink);
- }
- } else {
- Quotify.parseDeadlink.call(this, deadlink);
- }
+ Quotify.parseDeadlink.call(this, deadlink);
}
},
parseDeadlink: function(deadlink) {
@@ -11107,7 +11113,7 @@
},
mouseover: function(post) {
return function(e) {
- var el, error, file, height, isVideo, left, maxHeight, maxWidth, padding, ref, ref1, ref2, right, scale, width, x;
+ var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
if (!doc.contains(this)) {
return;
}
@@ -11151,9 +11157,8 @@
return results;
})(), width = ref1[0], height = ref1[1];
ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
- padding = 25;
maxWidth = Math.max(left, doc.clientWidth - right);
- maxHeight = doc.clientHeight - padding;
+ maxHeight = doc.clientHeight - UI.hover.padding;
scale = Math.min(1, maxWidth / width, maxHeight / height);
el.style.maxWidth = (scale * width) + "px";
el.style.maxHeight = (scale * height) + "px";
@@ -11162,10 +11167,7 @@
el: el,
latestEvent: e,
endEvents: 'mouseout click',
- asapTest: function() {
- return true;
- },
- height: scale * height + padding,
+ height: scale * height,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@@ -17286,7 +17288,7 @@
return;
}
$.extend(div, {
- innerHTML: "Filter is disabled.
Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:- Per boards, separate them with commas. It is global if not specified.
For example: boards:a,jp;. - Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).
For example: op:only;, op:no; or op:yes;. - Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).
For example: stub:yes; or stub:no;. - Highlight instead of hiding. You can specify a class name to use with a userstyle.
For example: highlight; or highlight:wallpaper;. - Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.
"
+ innerHTML: "Filter is disabled.
Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:- Per boards, separate them with commas. It is global if not specified.
For example: boards:a,jp;. - In case of a global rule, select boards to be excluded from the filter.
For example: exclude:vg,v;. - Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).
For example: op:only;, op:no; or op:yes;. - Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).
For example: stub:yes; or stub:no;. - Highlight instead of hiding. You can specify a class name to use with a userstyle.
For example: highlight; or highlight:wallpaper;. - Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.
"
});
return $('.warning', div).hidden = Conf['Filter'];
},
diff --git a/builds/4chan-X.crx b/builds/4chan-X.crx
index c9010d49d..39e7a99d9 100644
Binary files a/builds/4chan-X.crx and b/builds/4chan-X.crx differ
diff --git a/builds/4chan-X.meta.js b/builds/4chan-X.meta.js
index 29c4b6bb3..8eb8ee7ca 100644
--- a/builds/4chan-X.meta.js
+++ b/builds/4chan-X.meta.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
-// @version 1.11.20.3
+// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js
index 52854d28b..caacec022 100644
--- a/builds/4chan-X.user.js
+++ b/builds/4chan-X.user.js
@@ -1,7 +1,7 @@
// Generated by CoffeeScript
// ==UserScript==
// @name 4chan X
-// @version 1.11.20.3
+// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@@ -433,7 +433,7 @@
doc = d.documentElement;
g = {
- VERSION: '1.11.20.3',
+ VERSION: '1.11.21.0',
NAMESPACE: '4chan X.',
boards: {}
};
@@ -1404,6 +1404,8 @@
this.board = board1;
this.ID = +root.id.slice(2);
this.fullID = this.board + "." + this.ID;
+ this.context = this;
+ root.dataset.fullID = this.fullID;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@@ -1543,7 +1545,8 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
- if (!(match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/))) {
+ match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/);
+ if (!(match || (this.isClone && quotelink.dataset.postID))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@@ -1696,8 +1699,10 @@
Clone = (function(superClass) {
extend(Clone, superClass);
+ Clone.prototype.isClone = true;
+
function Clone(origin1, context1, contractThumb) {
- var file, info, inline, inlined, k, key, len1, len2, len3, nodes, post, q, ref, ref1, ref2, ref3, ref4, root, u, val;
+ var file, info, inline, inlined, k, key, len1, len2, len3, len4, node, nodes, post, q, ref, ref1, ref2, ref3, ref4, ref5, root, u, v, val;
this.origin = origin1;
this.context = context1;
ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'];
@@ -1707,6 +1712,13 @@
}
nodes = this.origin.nodes;
root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true);
+ Clone.prefix || (Clone.prefix = 0);
+ ref1 = [root].concat(slice.call($$('[id]', root)));
+ for (q = 0, len2 = ref1.length; q < len2; q++) {
+ node = ref1[q];
+ node.id = Clone.prefix + node.id;
+ }
+ Clone.prefix++;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@@ -1729,14 +1741,14 @@
} else {
this.nodes.backlinks = info.getElementsByClassName('backlink');
}
- ref1 = $$('.inline', post);
- for (q = 0, len2 = ref1.length; q < len2; q++) {
- inline = ref1[q];
+ ref2 = $$('.inline', post);
+ for (u = 0, len3 = ref2.length; u < len3; u++) {
+ inline = ref2[u];
$.rm(inline);
}
- ref2 = $$('.inlined', post);
- for (u = 0, len3 = ref2.length; u < len3; u++) {
- inlined = ref2[u];
+ ref3 = $$('.inlined', post);
+ for (v = 0, len4 = ref3.length; v < len4; v++) {
+ inlined = ref3[v];
$.rmClass(inlined, 'inlined');
}
root.hidden = false;
@@ -1767,11 +1779,12 @@
this.nodes.date = $('.dateTime', info);
}
this.parseQuotes();
+ this.quotes = slice.call(this.origin.quotes);
if (this.origin.file) {
this.file = {};
- ref3 = this.origin.file;
- for (key in ref3) {
- val = ref3[key];
+ ref4 = this.origin.file;
+ for (key in ref4) {
+ val = ref4[key];
this.file[key] = val;
}
file = $('.file', post);
@@ -1783,7 +1796,7 @@
if (this.file.videoThumb) {
this.file.thumb.muted = true;
}
- if ((ref4 = this.file.thumb) != null ? ref4.dataset.src : void 0) {
+ if ((ref5 = this.file.thumb) != null ? ref5.dataset.src : void 0) {
this.file.thumb.src = this.file.thumb.dataset.src;
this.file.thumb.removeAttribute('data-src');
}
@@ -1794,7 +1807,6 @@
if (this.origin.isDead) {
this.isDead = true;
}
- this.isClone = true;
root.dataset.clone = this.origin.clones.push(this) - 1;
}
@@ -2295,13 +2307,13 @@
})();
Fetcher = (function() {
- function Fetcher(boardID1, threadID1, postID1, root1, context1) {
+ function Fetcher(boardID1, threadID1, postID1, root1, quoter1) {
var post;
this.boardID = boardID1;
this.threadID = threadID1;
this.postID = postID1;
this.root = root1;
- this.context = context1;
+ this.quoter = quoter1;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
@@ -2319,15 +2331,23 @@
}
Fetcher.prototype.insert = function(post) {
- var clone, nodes;
+ var boardID, clone, k, len1, nodes, postID, quote, ref, ref1;
if (!this.root.parentNode) {
return;
}
- clone = post.addClone(this.context, $.hasClass(this.root, 'dialog'));
+ clone = post.addClone(this.quoter.context, $.hasClass(this.root, 'dialog'));
Main.callbackNodes(Clone, [clone]);
nodes = clone.nodes;
$.rmAll(nodes.root);
$.add(nodes.root, nodes.post);
+ ref = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
+ for (k = 0, len1 = ref.length; k < len1; k++) {
+ quote = ref[k];
+ ref1 = Get.postDataFromLink(quote), boardID = ref1.boardID, postID = ref1.postID;
+ if (postID === this.quoter.ID && boardID === this.quoter.board.ID) {
+ $.addClass(quote, 'forwardlink');
+ }
+ }
$.rmAll(this.root);
$.add(this.root, nodes.root);
return $.event('PostsInserted');
@@ -3089,10 +3109,10 @@
history.replaceState({}, '');
}
hash = this.location.hash.slice(1);
- if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) {
+ if (!(/^\d*p\d+$/.test(hash) && (post = $.id(hash)))) {
return;
}
- if ((Get.postFromRoot(post)).isHidden) {
+ if (!post.getBoundingClientRect().height) {
return;
}
return $.queueTask(function() {
@@ -4547,15 +4567,12 @@
return Get.threadFromRoot($.x('ancestor::div[@class="thread"]', node));
},
postFromRoot: function(root) {
- var boardID, index, link, post, postID;
+ var index, post;
if (root == null) {
return null;
}
- link = $('.postNum > a[href*="#"]', root);
- boardID = link.pathname.split(/\/+/)[1];
- postID = link.hash.slice(2);
+ post = g.posts[root.dataset.fullID];
index = root.dataset.clone;
- post = g.posts[boardID + "." + postID];
if (index) {
return post.clones[index];
} else {
@@ -4565,9 +4582,6 @@
postFromNode: function(root) {
return Get.postFromRoot($.x('(ancestor::div[contains(@class,"postContainer")][1]|following::div[contains(@class,"postContainer")][1])', root));
},
- contextFromNode: function(node) {
- return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', node));
- },
postDataFromLink: function(link) {
var boardID, path, postID, ref, threadID;
if (link.hostname === 'boards.4chan.org') {
@@ -4996,8 +5010,8 @@
return $.set(this.id + ".position", this.style.cssText);
};
hoverstart = function(arg) {
- var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
- root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, asapTest = arg.asapTest, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
+ var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
+ root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
o = {
root: root,
el: el,
@@ -5013,12 +5027,13 @@
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
- $.asap(function() {
- return !el.parentNode || asapTest();
- }, function() {
+ o.hover(o.latestEvent);
+ new MutationObserver(function() {
if (el.parentNode) {
return o.hover(o.latestEvent);
}
+ }).observe(el, {
+ childList: true
});
$.on(root, endEvents, o.hoverend);
if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) {
@@ -5032,10 +5047,11 @@
};
return $.on(doc, 'mousemove', o.workaround);
};
+ hoverstart.padding = 25;
hover = function(e) {
var clientX, clientY, height, left, ref, right, style, threshold, top;
this.latestEvent = e;
- height = this.height || this.el.offsetHeight;
+ height = (this.height || this.el.offsetHeight) + hoverstart.padding;
clientX = e.clientX, clientY = e.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
@@ -5249,7 +5265,7 @@
Filter = {
filters: {},
init: function() {
- var boards, err, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, regexp, stub, top;
+ var boards, err, excludes, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, stub, top;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
@@ -5270,6 +5286,9 @@
filter = line.replace(regexp[0], '');
boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
boards = boards === 'global' ? null : boards.split(',');
+ if (boards === null) {
+ excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase().split(',') : void 0) || null;
+ }
if (key === 'uniqueID' || key === 'MD5') {
regexp = regexp[1];
} else {
@@ -5281,10 +5300,10 @@
continue;
}
}
- op = ((ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref3[1] : void 0) || 'yes';
+ op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
stub = (function() {
- var ref4;
- switch ((ref4 = filter.match(/stub:(yes|no)/)) != null ? ref4[1] : void 0) {
+ var ref5;
+ switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
case 'yes':
return true;
case 'no':
@@ -5294,11 +5313,11 @@
}
})();
if (hl = /highlight/.test(filter)) {
- hl = ((ref4 = filter.match(/highlight:([\w-]+)/)) != null ? ref4[1] : void 0) || 'filter-highlight';
- top = ((ref5 = filter.match(/top:(yes|no)/)) != null ? ref5[1] : void 0) || 'yes';
+ hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
+ top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
top = top === 'yes';
}
- this.filters[key].push(this.createFilter(regexp, boards, op, stub, hl, top));
+ this.filters[key].push(this.createFilter(regexp, boards, excludes, op, stub, hl, top));
}
if (!this.filters[key].length) {
delete this.filters[key];
@@ -5312,7 +5331,7 @@
cb: this.node
});
},
- createFilter: function(regexp, boards, op, stub, hl, top) {
+ createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
var settings, test;
test = typeof regexp === 'string' ? function(value) {
return regexp === value;
@@ -5329,6 +5348,9 @@
if (boards && indexOf.call(boards, boardID) < 0) {
return false;
}
+ if (excludes && indexOf.call(excludes, boardID) >= 0) {
+ return false;
+ }
if (isReply && op === 'only' || !isReply && op === 'no') {
return false;
}
@@ -6259,7 +6281,7 @@
if (this.isClone && this.thread === this.context.thread) {
return;
}
- ref = this.isClone ? this.context : this, board = ref.board, thread = ref.thread;
+ ref = this.context, board = ref.board, thread = ref.thread;
ref1 = this.nodes.quotelinks;
for (k = 0, len1 = ref1.length; k < len1; k++) {
quotelink = ref1[k];
@@ -6330,20 +6352,21 @@
});
},
toggle: function(e) {
- var boardID, context, postID, ref, threadID;
+ var boardID, context, postID, quoter, ref, threadID;
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
return;
}
e.preventDefault();
ref = Get.postDataFromLink(this), boardID = ref.boardID, threadID = ref.threadID, postID = ref.postID;
- context = Get.contextFromNode(this);
+ quoter = Get.postFromNode(this);
+ context = quoter.context;
if ($.hasClass(this, 'inlined')) {
QuoteInline.rm(this, boardID, threadID, postID, context);
} else {
- if ($.x("ancestor::div[@id='pc" + postID + "']", this)) {
+ if ($.x("ancestor::div[@data-full-i-d='" + boardID + "." + postID + "']", this)) {
return;
}
- QuoteInline.add(this, boardID, threadID, postID, context);
+ QuoteInline.add(this, boardID, threadID, postID, context, quoter);
}
return this.classList.toggle('inlined');
},
@@ -6354,18 +6377,17 @@
return $.x('ancestor-or-self::*[parent::blockquote][1]', quotelink);
}
},
- add: function(quotelink, boardID, threadID, postID, context) {
+ add: function(quotelink, boardID, threadID, postID, context, quoter) {
var inline, isBacklink, post, qroot, root;
isBacklink = $.hasClass(quotelink, 'backlink');
inline = $.el('div', {
- id: "i" + postID,
className: 'inline'
});
root = QuoteInline.findRoot(quotelink, isBacklink);
$.after(root, inline);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.addClass(qroot, 'hasInline');
- new Fetcher(boardID, threadID, postID, inline, context);
+ new Fetcher(boardID, threadID, postID, inline, quoter);
if (!((post = g.posts[boardID + "." + postID]) && context.thread === post.thread)) {
return;
}
@@ -6382,7 +6404,7 @@
var el, inlined, isBacklink, post, qroot, ref, root;
isBacklink = $.hasClass(quotelink, 'backlink');
root = QuoteInline.findRoot(quotelink, isBacklink);
- root = $.x("following-sibling::div[@id='i" + postID + "'][1]", root);
+ root = $.x("following-sibling::div[div/@data-full-i-d='" + boardID + "." + postID + "'][1]", root);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.rm(root);
if (!$('.inline', qroot)) {
@@ -6435,7 +6457,7 @@
quotelink.textContent = quotelink.textContent.replace(QuoteOP.text, '');
}
}
- fullID = (this.isClone ? this.context : this).thread.fullID;
+ fullID = this.context.thread.fullID;
if (indexOf.call(quotes, fullID) < 0) {
return;
}
@@ -6472,7 +6494,7 @@
}
},
mouseover: function(e) {
- var boardID, clone, k, len1, len2, origin, post, postID, posts, q, qp, quote, quoterID, ref, ref1, threadID;
+ var boardID, k, len1, origin, post, postID, posts, qp, ref, threadID;
if ($.hasClass(this, 'inlined') || !d.contains(this)) {
return;
}
@@ -6482,21 +6504,15 @@
className: 'dialog'
});
$.add(Header.hover, qp);
- new Fetcher(boardID, threadID, postID, qp, Get.contextFromNode(this));
+ new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this));
UI.hover({
root: this,
el: qp,
latestEvent: e,
endEvents: 'mouseout click',
- cb: QuotePreview.mouseout,
- asapTest: function() {
- return qp.firstElementChild;
- }
+ cb: QuotePreview.mouseout
});
- if (!(origin = g.posts[boardID + "." + postID])) {
- return;
- }
- if (Conf['Quote Highlighting']) {
+ if (Conf['Quote Highlighting'] && (origin = g.posts[boardID + "." + postID])) {
posts = [origin].concat(origin.clones);
posts.pop();
for (k = 0, len1 = posts.length; k < len1; k++) {
@@ -6504,15 +6520,6 @@
$.addClass(post.nodes.post, 'qphl');
}
}
- quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0];
- clone = Get.postFromRoot(qp.firstChild);
- ref1 = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
- for (q = 0, len2 = ref1.length; q < len2; q++) {
- quote = ref1[q];
- if (quote.hash.slice(2) === quoterID) {
- $.addClass(quote, 'forwardlink');
- }
- }
},
mouseout: function() {
var clone, k, len1, post, ref, root;
@@ -6802,7 +6809,7 @@
if (highlight = $('.highlight')) {
$.rmClass(highlight, 'highlight');
}
- if (!QuoteYou.lastRead) {
+ if (!(QuoteYou.lastRead && doc.contains(QuoteYou.lastRead) && $.hasClass(QuoteYou.lastRead, 'quotesYou'))) {
if (!(post = QuoteYou.lastRead = $('.quotesYou'))) {
new Notice('warning', 'No posts are currently quoting you, loser.', 20);
return;
@@ -6814,7 +6821,7 @@
post = QuoteYou.lastRead;
}
str = type + "::div[contains(@class,'quotesYou')]";
- while (post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) {
+ while ((post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0))) {
if (QuoteYou.cb.scroll(post)) {
return;
}
@@ -6822,14 +6829,16 @@
posts = $$('.quotesYou');
return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]);
},
- scroll: function(post) {
- if (Get.postFromRoot(post).isHidden) {
+ scroll: function(root) {
+ var post;
+ post = $('.post', root);
+ if (!post.getBoundingClientRect().height) {
return false;
} else {
- QuoteYou.lastRead = post;
+ QuoteYou.lastRead = root;
window.location = "#" + post.id;
Header.scrollTo(post);
- $.addClass($('.post', post), 'highlight');
+ $.addClass(post, 'highlight');
return true;
}
}
@@ -6852,16 +6861,13 @@
},
node: function() {
var deadlink, k, len1, ref;
+ if (this.isClone) {
+ return;
+ }
ref = $$('.deadlink', this.nodes.comment);
for (k = 0, len1 = ref.length; k < len1; k++) {
deadlink = ref[k];
- if (this.isClone) {
- if ($.hasClass(deadlink, 'quotelink')) {
- this.nodes.quotelinks.push(deadlink);
- }
- } else {
- Quotify.parseDeadlink.call(this, deadlink);
- }
+ Quotify.parseDeadlink.call(this, deadlink);
}
},
parseDeadlink: function(deadlink) {
@@ -11107,7 +11113,7 @@
},
mouseover: function(post) {
return function(e) {
- var el, error, file, height, isVideo, left, maxHeight, maxWidth, padding, ref, ref1, ref2, right, scale, width, x;
+ var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
if (!doc.contains(this)) {
return;
}
@@ -11151,9 +11157,8 @@
return results;
})(), width = ref1[0], height = ref1[1];
ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
- padding = 25;
maxWidth = Math.max(left, doc.clientWidth - right);
- maxHeight = doc.clientHeight - padding;
+ maxHeight = doc.clientHeight - UI.hover.padding;
scale = Math.min(1, maxWidth / width, maxHeight / height);
el.style.maxWidth = (scale * width) + "px";
el.style.maxHeight = (scale * height) + "px";
@@ -11162,10 +11167,7 @@
el: el,
latestEvent: e,
endEvents: 'mouseout click',
- asapTest: function() {
- return true;
- },
- height: scale * height + padding,
+ height: scale * height,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@@ -17286,7 +17288,7 @@
return;
}
$.extend(div, {
- innerHTML: "Filter is disabled.
Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:- Per boards, separate them with commas. It is global if not specified.
For example: boards:a,jp;. - Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).
For example: op:only;, op:no; or op:yes;. - Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).
For example: stub:yes; or stub:no;. - Highlight instead of hiding. You can specify a class name to use with a userstyle.
For example: highlight; or highlight:wallpaper;. - Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.
"
+ innerHTML: "Filter is disabled.
Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:- Per boards, separate them with commas. It is global if not specified.
For example: boards:a,jp;. - In case of a global rule, select boards to be excluded from the filter.
For example: exclude:vg,v;. - Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).
For example: op:only;, op:no; or op:yes;. - Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).
For example: stub:yes; or stub:no;. - Highlight instead of hiding. You can specify a class name to use with a userstyle.
For example: highlight; or highlight:wallpaper;. - Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.
"
});
return $('.warning', div).hidden = Conf['Filter'];
},
diff --git a/builds/4chan-X.zip b/builds/4chan-X.zip
index 880916756..55a6eeee8 100644
Binary files a/builds/4chan-X.zip and b/builds/4chan-X.zip differ
diff --git a/builds/updates-beta.xml b/builds/updates-beta.xml
index 163054b97..652364fcb 100644
--- a/builds/updates-beta.xml
+++ b/builds/updates-beta.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/builds/updates.xml b/builds/updates.xml
index 7704acc6d..07731810f 100644
--- a/builds/updates.xml
+++ b/builds/updates.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/version.json b/version.json
index 84383d1e7..8d2781e21 100644
--- a/version.json
+++ b/version.json
@@ -1,4 +1,4 @@
{
- "version": "1.11.20.3",
- "date": "2015-12-11T08:28:46.605Z"
+ "version": "1.11.21.0",
+ "date": "2015-12-14T04:31:24.432Z"
}