diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea9c19aa9..ef39728ca 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+
+**MayhemYDG**:
+- New desktop notification:
+ - The QR will now warn you when you are running low on cached captchas while auto-posting.
+
### v1.2.35
*2013-08-20*
diff --git a/Gruntfile.coffee b/Gruntfile.coffee
index 31fc9179c..61e3d9071 100755
--- a/Gruntfile.coffee
+++ b/Gruntfile.coffee
@@ -30,6 +30,7 @@ module.exports = (grunt) ->
'src/Linkification/**/*'
'src/Posting/**/*'
'src/Images/**/*'
+ 'src/Linkification/**/*'
'src/Menu/**/*'
'src/Monitoring/**/*'
'src/Archive/**/*'
diff --git a/LICENSE b/LICENSE
index 86d30ac20..3f67fe3e0 100755
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
/*
-* 4chan X - Version 1.2.35 - 2013-08-20
+* 4chan X - Version 1.2.35 - 2013-08-22
*
* Licensed under the MIT license.
* https://github.com/seaweedchan/4chan-x/blob/master/LICENSE
diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js
index 36b5f35aa..3e4d8bda7 100644
--- a/builds/4chan-X.user.js
+++ b/builds/4chan-X.user.js
@@ -22,7 +22,7 @@
// ==/UserScript==
/*
-* 4chan X - Version 1.2.35 - 2013-08-20
+* 4chan X - Version 1.2.35 - 2013-08-22
*
* Licensed under the MIT license.
* https://github.com/seaweedchan/4chan-x/blob/master/LICENSE
@@ -945,6 +945,7 @@
Post.prototype.parseComment = function() {
var bq, i, node, nodes, text;
+ this.nodes.comment.normalize();
bq = this.nodes.comment.cloneNode(true);
nodes = $$('.abbr, .capcodeReplies, .exif, b', bq);
i = 0;
@@ -1872,7 +1873,7 @@
return;
}
el = $.el('span', {
- innerHTML: "Desktop notification permissions are not granted:
\n or "
+ innerHTML: "Desktop notification permissions are not granted.\n[FAQ]
\n or "
});
_ref = $$('button', el), authorize = _ref[0], disable = _ref[1];
$.on(authorize, 'click', function() {
@@ -5505,7 +5506,7 @@
}
e.preventDefault();
QR.open();
- QR.fileInput(e.dataTransfer.files);
+ QR.handleFiles(e.dataTransfer.files);
return $.addClass(QR.nodes.el, 'dump');
},
paste: function(e) {
@@ -5515,84 +5516,72 @@
_ref = e.clipboardData.items;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
item = _ref[_i];
- if (item.kind === 'file') {
- blob = item.getAsFile();
- blob.name = 'file';
- if (blob.type) {
- blob.name += '.' + blob.type.split('/')[1];
- }
- files.push(blob);
+ if (!(item.kind === 'file')) {
+ continue;
}
+ blob = item.getAsFile();
+ blob.name = 'file';
+ if (blob.type) {
+ blob.name += '.' + blob.type.split('/')[1];
+ }
+ files.push(blob);
}
if (!files.length) {
return;
}
QR.open();
- return QR.fileInput(files);
+ QR.handleFiles(files);
+ return $.addClass(QR.nodes.el, 'dump');
},
- openFileInput: function(e) {
- e.stopPropagation();
- if (e.shiftKey && e.type === 'click') {
- return QR.selected.rmFile();
- }
- if (e.ctrlKey && e.type === 'click') {
- $.addClass(QR.nodes.filename, 'edit');
- QR.nodes.filename.focus();
- return $.on(QR.nodes.filename, 'blur', function() {
- return $.rmClass(QR.nodes.filename, 'edit');
- });
- }
- if (e.target.nodeName === 'INPUT' || (e.keyCode && ![32, 13].contains(e.keyCode)) || e.ctrlKey) {
- return;
- }
- e.preventDefault();
- return QR.nodes.fileInput.click();
- },
- fileInput: function(files) {
- var file, length, max, post, _i, _len;
+ handleFiles: function(files) {
+ var file, isSingle, max, _i, _len;
- if (this instanceof Element) {
+ if (this !== QR) {
files = __slice.call(this.files);
- QR.nodes.fileInput.value = null;
+ this.value = null;
}
- length = files.length;
- if (!length) {
+ if (!files.length) {
return;
}
max = QR.nodes.fileInput.max;
+ isSingle = files.length === 1;
QR.cleanNotifications();
- if (length === 1) {
- file = files[0];
- if (/^text/.test(file.type)) {
- QR.selected.pasteText(file);
- } else if (file.size > max) {
- QR.error("File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
- } else if (!QR.mimeTypes.contains(file.type)) {
- QR.error('Unsupported file type.');
- } else {
- QR.selected.setFile(file);
- }
- return;
- }
for (_i = 0, _len = files.length; _i < _len; _i++) {
file = files[_i];
- if (/^text/.test(file.type)) {
- if ((post = QR.posts[QR.posts.length - 1]).com) {
- post = new QR.post();
- }
- post.pasteText(file);
- } else if (file.size > max) {
- QR.error("" + file.name + ": File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
- } else if (!QR.mimeTypes.contains(file.type)) {
- QR.error("" + file.name + ": Unsupported file type.");
- } else {
- if ((post = QR.posts[QR.posts.length - 1]).file) {
- post = new QR.post();
- }
- post.setFile(file);
- }
+ QR.handleFile(file, isSingle, max);
}
- return $.addClass(QR.nodes.el, 'dump');
+ if (!isSingle) {
+ return $.addClass(QR.nodes.el, 'dump');
+ }
+ },
+ handleFile: function(file, isSingle, max) {
+ var post;
+
+ if (file.size > max) {
+ QR.error("" + file.name + ": File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
+ return;
+ } else if (!QR.mimeTypes.contains(file.type)) {
+ if (!/^text/.test(file.type)) {
+ QR.error("" + file.name + ": Unsupported file type.");
+ return;
+ }
+ if (isSingle) {
+ post = QR.selected;
+ } else if ((post = QR.posts[QR.posts.length - 1]).com) {
+ post = new QR.post();
+ }
+ post.pasteText(file);
+ return;
+ }
+ if (isSingle) {
+ post = QR.selected;
+ } else if ((post = QR.posts[QR.posts.length - 1]).file) {
+ post = new QR.post();
+ }
+ return post.setFile(file);
+ },
+ openFileInput: function() {
+ return QR.nodes.fileInput.click();
},
posts: [],
post: (function() {
@@ -5892,7 +5881,8 @@
return reader.readAsText(file);
};
- _Class.prototype.dragStart = function() {
+ _Class.prototype.dragStart = function(e) {
+ e.dataTransfer.setDragImage(this, e.layerX, e.layerY);
return $.addClass(this, 'drag');
};
@@ -6198,7 +6188,7 @@
$.on(nodes.spoiler, 'change', function() {
return QR.selected.nodes.spoiler.click();
});
- $.on(nodes.fileInput, 'change', QR.fileInput);
+ $.on(nodes.fileInput, 'change', QR.handleFiles);
items = ['name', 'email', 'sub', 'com', 'filename'];
i = 0;
while (name = items[i++]) {
@@ -6344,7 +6334,7 @@
return QR.status();
},
response: function() {
- var URL, ban, board, err, h1, isReply, m, post, postID, req, resDoc, threadID, _, _ref, _ref1;
+ var URL, ban, board, captchasCount, err, h1, isReply, m, notif, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
req = QR.req;
delete QR.req;
@@ -6408,7 +6398,22 @@
threadID: threadID,
postID: postID
});
- QR.cooldown.auto = QR.posts.length > 1 && isReply;
+ postsCount = QR.posts.length;
+ QR.cooldown.auto = postsCount > 1 && isReply;
+ if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
+ notif = new Notification('Quick reply warning', {
+ body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
+ icon: Favicon.logo
+ });
+ notif.onclick = function() {
+ QR.open();
+ QR.captcha.nodes.input.focus();
+ return window.focus();
+ };
+ setTimeout(function() {
+ return notif.close();
+ }, 7 * $.SECOND);
+ }
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
QR.close();
} else {
@@ -7367,7 +7372,6 @@
return Favicon.unreadY = Favicon.unreadNSFWY;
}
},
- empty: 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==',
dead: 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAIALAAAAAAQABAAAAIvlI+pq+D9DAgUoFkPDlbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==',
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAgMAAAC+UIlYAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAGlJREFUWMPtlkEKADEIA/tJP9lXLttQto2yHxgDHozTi0ToGK2WKZZ+HAQQMZc+xBwI4EZ+wAC2IfPuSIDOZJrSZQEAX9eVJhhwIuUYAnQe8rhAEMAZlTI2MID9f5Clyh0JeE1V1ZEAvB4qDfwuJTSGRAAAAABJRU5ErkJggg=='
};
@@ -8168,7 +8172,7 @@
return div;
},
refresh: function() {
- var boardID, data, list, nodes, refresher, thread, threadID, toggler, watched, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
+ var boardID, data, helper, list, nodes, refresher, thread, threadID, toggler, watched, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
nodes = [];
_ref = ThreadWatcher.getAll();
@@ -8187,7 +8191,9 @@
boardID: thread.board.ID,
threadID: threadID
});
- $[watched ? 'addClass' : 'rmClass'](toggler, 'watched');
+ helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
+ $[helper[0]](toggler, 'watched');
+ toggler.title = "" + helper[1] + " Thread";
}
_ref3 = ThreadWatcher.menu.refreshers;
for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
@@ -8390,7 +8396,7 @@
Unread = {
init: function() {
- if (g.VIEW !== 'thread' || !Conf['Unread Count'] && !Conf['Unread Favicon']) {
+ if (g.VIEW !== 'thread' || !Conf['Unread Count'] && !Conf['Unread Favicon'] && !Conf['Desktop Notifications']) {
return;
}
this.db = new DataBoard('lastReadPosts', this.sync);
@@ -8432,18 +8438,18 @@
}
}
Unread.addPosts(posts);
- if (Conf['Scroll to Last Read Post']) {
- return Unread.scroll();
- }
+ return Unread.scroll();
},
scroll: function() {
var checkPosition, hash, onload, post, posts, root;
+ if (!Conf['Scroll to Last Read Post']) {
+ return;
+ }
if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) {
return;
}
- if (Unread.posts.length) {
- post = Unread.posts[0];
+ if (post = Unread.posts[0]) {
while (root = $.x('preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root)) {
if (!(post = Get.postFromRoot(root)).isHidden) {
break;
@@ -8467,10 +8473,7 @@
};
}
checkPosition = function(target) {
- var height, top, _ref;
-
- _ref = target.getBoundingClientRect(), top = _ref.top, height = _ref.height;
- return top + height - doc.clientHeight > 0;
+ return target.getBoundingClientRect().bottom > doc.clientHeight;
};
return $.on(window, 'load', onload);
},
@@ -8488,7 +8491,9 @@
Unread.lastReadPost = lastReadPost;
Unread.readArray(Unread.posts);
Unread.readArray(Unread.postsQuotingYou);
- Unread.setLine();
+ if (Conf['Unread Line']) {
+ Unread.setLine();
+ }
return Unread.update();
},
addPosts: function(posts) {
@@ -8533,7 +8538,6 @@
}
Unread.postsQuotingYou.push(post);
Unread.openNotification(post);
- return;
}
},
openNotification: function(post) {
@@ -8553,7 +8557,7 @@
};
return setTimeout(function() {
return notif.close();
- }, 5 * $.SECOND);
+ }, 7 * $.SECOND);
},
onUpdate: function(e) {
if (e.detail[404]) {
@@ -8590,7 +8594,7 @@
return arr.splice(0, i);
},
read: $.debounce(50, function(e) {
- var ID, bottom, height, i, post, posts;
+ var ID, height, i, post, posts;
if (d.hidden || !Unread.posts.length) {
return;
@@ -8599,8 +8603,7 @@
posts = Unread.posts;
i = 0;
while (post = posts[i]) {
- bottom = post.nodes.root.getBoundingClientRect().bottom;
- if (bottom < height) {
+ if (post.nodes.root.getBoundingClientRect().bottom < height) {
ID = post.ID;
if (Conf['Mark Quotes of You']) {
if (post.info.yours) {
@@ -8618,10 +8621,8 @@
}
i++;
}
- if (!Conf['Quote Threading']) {
- if (i) {
- posts.splice(0, i);
- }
+ if (i && !Conf['Quote Threading']) {
+ posts.splice(0, i);
}
if (!ID) {
return;
@@ -8646,19 +8647,17 @@
});
}),
setLine: function(force) {
- var post, root;
+ var post;
if (!(d.hidden || force === true)) {
return;
}
- if (post = Unread.posts[0]) {
- root = post.nodes.root;
- if (root !== $('.thread > .replyContainer', root.parentNode)) {
- return $.before(root, Unread.hr);
- }
- } else {
+ if (!(post = Unread.posts[0])) {
return $.rm(Unread.hr);
}
+ if ($.x('preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root)) {
+ return $.before(post.nodes.root, Unread.hr);
+ }
},
update: function() {
var count;
@@ -8670,7 +8669,7 @@
if (!Conf['Unread Favicon']) {
return;
}
- Favicon.el.href = g.DEAD ? Unread.postsQuotingYou.length ? Favicon.unreadDeadY : count ? Favicon.unreadDead : Favicon.dead : count ? Unread.postsQuotingYou.length ? Favicon.unreadY : Favicon.unread : Favicon["default"];
+ Favicon.el.href = g.DEAD ? Unread.postsQuotingYou[0] ? Favicon.unreadDeadY : count ? Favicon.unreadDead : Favicon.dead : count ? Unread.postsQuotingYou[0] ? Favicon.unreadY : Favicon.unread : Favicon["default"];
return $.add(d.head, Favicon.el);
}
};
@@ -11291,6 +11290,7 @@
} catch (_error) {
err = _error;
new Notice('warning', 'Cookies need to be enabled on 4chan for 4chan X to properly function.', 30);
+ Main.disableReports = true;
}
return $.event('4chanXInitFinished');
},
diff --git a/builds/crx/script.js b/builds/crx/script.js
index a9db4a21f..a75308436 100644
--- a/builds/crx/script.js
+++ b/builds/crx/script.js
@@ -1,6 +1,6 @@
// Generated by CoffeeScript
/*
-* 4chan X - Version 1.2.35 - 2013-08-20
+* 4chan X - Version 1.2.35 - 2013-08-22
*
* Licensed under the MIT license.
* https://github.com/seaweedchan/4chan-x/blob/master/LICENSE
@@ -954,6 +954,7 @@
Post.prototype.parseComment = function() {
var bq, i, node, nodes, text;
+ this.nodes.comment.normalize();
bq = this.nodes.comment.cloneNode(true);
nodes = $$('.abbr, .capcodeReplies, .exif, b', bq);
i = 0;
@@ -1886,7 +1887,7 @@
return;
}
el = $.el('span', {
- innerHTML: "Desktop notification permissions are not granted:
\n or "
+ innerHTML: "Desktop notification permissions are not granted.\n[FAQ]
\n or "
});
_ref = $$('button', el), authorize = _ref[0], disable = _ref[1];
$.on(authorize, 'click', function() {
@@ -5179,7 +5180,7 @@
return setTimeout(function() {
notif.onclose = null;
return notif.close();
- }, 5 * $.SECOND);
+ }, 7 * $.SECOND);
},
notifications: [],
cleanNotifications: function() {
@@ -5515,7 +5516,7 @@
}
e.preventDefault();
QR.open();
- QR.fileInput(e.dataTransfer.files);
+ QR.handleFiles(e.dataTransfer.files);
return $.addClass(QR.nodes.el, 'dump');
},
paste: function(e) {
@@ -5525,84 +5526,72 @@
_ref = e.clipboardData.items;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
item = _ref[_i];
- if (item.kind === 'file') {
- blob = item.getAsFile();
- blob.name = 'file';
- if (blob.type) {
- blob.name += '.' + blob.type.split('/')[1];
- }
- files.push(blob);
+ if (!(item.kind === 'file')) {
+ continue;
}
+ blob = item.getAsFile();
+ blob.name = 'file';
+ if (blob.type) {
+ blob.name += '.' + blob.type.split('/')[1];
+ }
+ files.push(blob);
}
if (!files.length) {
return;
}
QR.open();
- return QR.fileInput(files);
+ QR.handleFiles(files);
+ return $.addClass(QR.nodes.el, 'dump');
},
- openFileInput: function(e) {
- e.stopPropagation();
- if (e.shiftKey && e.type === 'click') {
- return QR.selected.rmFile();
- }
- if (e.ctrlKey && e.type === 'click') {
- $.addClass(QR.nodes.filename, 'edit');
- QR.nodes.filename.focus();
- return $.on(QR.nodes.filename, 'blur', function() {
- return $.rmClass(QR.nodes.filename, 'edit');
- });
- }
- if (e.target.nodeName === 'INPUT' || (e.keyCode && ![32, 13].contains(e.keyCode)) || e.ctrlKey) {
- return;
- }
- e.preventDefault();
- return QR.nodes.fileInput.click();
- },
- fileInput: function(files) {
- var file, length, max, post, _i, _len;
+ handleFiles: function(files) {
+ var file, isSingle, max, _i, _len;
- if (this instanceof Element) {
+ if (this !== QR) {
files = __slice.call(this.files);
- QR.nodes.fileInput.value = null;
+ this.value = null;
}
- length = files.length;
- if (!length) {
+ if (!files.length) {
return;
}
max = QR.nodes.fileInput.max;
+ isSingle = files.length === 1;
QR.cleanNotifications();
- if (length === 1) {
- file = files[0];
- if (/^text/.test(file.type)) {
- QR.selected.pasteText(file);
- } else if (file.size > max) {
- QR.error("File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
- } else if (!QR.mimeTypes.contains(file.type)) {
- QR.error('Unsupported file type.');
- } else {
- QR.selected.setFile(file);
- }
- return;
- }
for (_i = 0, _len = files.length; _i < _len; _i++) {
file = files[_i];
- if (/^text/.test(file.type)) {
- if ((post = QR.posts[QR.posts.length - 1]).com) {
- post = new QR.post();
- }
- post.pasteText(file);
- } else if (file.size > max) {
- QR.error("" + file.name + ": File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
- } else if (!QR.mimeTypes.contains(file.type)) {
- QR.error("" + file.name + ": Unsupported file type.");
- } else {
- if ((post = QR.posts[QR.posts.length - 1]).file) {
- post = new QR.post();
- }
- post.setFile(file);
- }
+ QR.handleFile(file, isSingle, max);
}
- return $.addClass(QR.nodes.el, 'dump');
+ if (!isSingle) {
+ return $.addClass(QR.nodes.el, 'dump');
+ }
+ },
+ handleFile: function(file, isSingle, max) {
+ var post;
+
+ if (file.size > max) {
+ QR.error("" + file.name + ": File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
+ return;
+ } else if (!QR.mimeTypes.contains(file.type)) {
+ if (!/^text/.test(file.type)) {
+ QR.error("" + file.name + ": Unsupported file type.");
+ return;
+ }
+ if (isSingle) {
+ post = QR.selected;
+ } else if ((post = QR.posts[QR.posts.length - 1]).com) {
+ post = new QR.post();
+ }
+ post.pasteText(file);
+ return;
+ }
+ if (isSingle) {
+ post = QR.selected;
+ } else if ((post = QR.posts[QR.posts.length - 1]).file) {
+ post = new QR.post();
+ }
+ return post.setFile(file);
+ },
+ openFileInput: function() {
+ return QR.nodes.fileInput.click();
},
posts: [],
post: (function() {
@@ -5896,7 +5885,8 @@
return reader.readAsText(file);
};
- _Class.prototype.dragStart = function() {
+ _Class.prototype.dragStart = function(e) {
+ e.dataTransfer.setDragImage(this, e.layerX, e.layerY);
return $.addClass(this, 'drag');
};
@@ -6194,7 +6184,7 @@
$.on(nodes.spoiler, 'change', function() {
return QR.selected.nodes.spoiler.click();
});
- $.on(nodes.fileInput, 'change', QR.fileInput);
+ $.on(nodes.fileInput, 'change', QR.handleFiles);
items = ['name', 'email', 'sub', 'com', 'filename'];
i = 0;
while (name = items[i++]) {
@@ -6329,7 +6319,7 @@
return QR.status();
},
response: function() {
- var URL, ban, board, err, h1, isReply, m, post, postID, req, resDoc, threadID, _, _ref, _ref1;
+ var URL, ban, board, captchasCount, err, h1, isReply, m, notif, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
req = QR.req;
delete QR.req;
@@ -6393,7 +6383,22 @@
threadID: threadID,
postID: postID
});
- QR.cooldown.auto = QR.posts.length > 1 && isReply;
+ postsCount = QR.posts.length;
+ QR.cooldown.auto = postsCount > 1 && isReply;
+ if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
+ notif = new Notification('Quick reply warning', {
+ body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
+ icon: Favicon.logo
+ });
+ notif.onclick = function() {
+ QR.open();
+ QR.captcha.nodes.input.focus();
+ return window.focus();
+ };
+ setTimeout(function() {
+ return notif.close();
+ }, 7 * $.SECOND);
+ }
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
QR.close();
} else {
@@ -7352,7 +7357,6 @@
return Favicon.unreadY = Favicon.unreadNSFWY;
}
},
- empty: 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==',
dead: 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAIALAAAAAAQABAAAAIvlI+pq+D9DAgUoFkPDlbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==',
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAgMAAAC+UIlYAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAGlJREFUWMPtlkEKADEIA/tJP9lXLttQto2yHxgDHozTi0ToGK2WKZZ+HAQQMZc+xBwI4EZ+wAC2IfPuSIDOZJrSZQEAX9eVJhhwIuUYAnQe8rhAEMAZlTI2MID9f5Clyh0JeE1V1ZEAvB4qDfwuJTSGRAAAAABJRU5ErkJggg=='
};
@@ -8153,7 +8157,7 @@
return div;
},
refresh: function() {
- var boardID, data, list, nodes, refresher, thread, threadID, toggler, watched, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
+ var boardID, data, helper, list, nodes, refresher, thread, threadID, toggler, watched, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
nodes = [];
_ref = ThreadWatcher.getAll();
@@ -8172,7 +8176,9 @@
boardID: thread.board.ID,
threadID: threadID
});
- $[watched ? 'addClass' : 'rmClass'](toggler, 'watched');
+ helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch'];
+ $[helper[0]](toggler, 'watched');
+ toggler.title = "" + helper[1] + " Thread";
}
_ref3 = ThreadWatcher.menu.refreshers;
for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
@@ -8375,7 +8381,7 @@
Unread = {
init: function() {
- if (g.VIEW !== 'thread' || !Conf['Unread Count'] && !Conf['Unread Favicon']) {
+ if (g.VIEW !== 'thread' || !Conf['Unread Count'] && !Conf['Unread Favicon'] && !Conf['Desktop Notifications']) {
return;
}
this.db = new DataBoard('lastReadPosts', this.sync);
@@ -8417,18 +8423,18 @@
}
}
Unread.addPosts(posts);
- if (Conf['Scroll to Last Read Post']) {
- return Unread.scroll();
- }
+ return Unread.scroll();
},
scroll: function() {
var checkPosition, hash, onload, post, posts, root;
+ if (!Conf['Scroll to Last Read Post']) {
+ return;
+ }
if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) {
return;
}
- if (Unread.posts.length) {
- post = Unread.posts[0];
+ if (post = Unread.posts[0]) {
while (root = $.x('preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root)) {
if (!(post = Get.postFromRoot(root)).isHidden) {
break;
@@ -8452,10 +8458,7 @@
};
}
checkPosition = function(target) {
- var height, top, _ref;
-
- _ref = target.getBoundingClientRect(), top = _ref.top, height = _ref.height;
- return top + height - doc.clientHeight > 0;
+ return target.getBoundingClientRect().bottom > doc.clientHeight;
};
return $.on(window, 'load', onload);
},
@@ -8473,7 +8476,9 @@
Unread.lastReadPost = lastReadPost;
Unread.readArray(Unread.posts);
Unread.readArray(Unread.postsQuotingYou);
- Unread.setLine();
+ if (Conf['Unread Line']) {
+ Unread.setLine();
+ }
return Unread.update();
},
addPosts: function(posts) {
@@ -8518,7 +8523,6 @@
}
Unread.postsQuotingYou.push(post);
Unread.openNotification(post);
- return;
}
},
openNotification: function(post) {
@@ -8538,7 +8542,7 @@
};
return setTimeout(function() {
return notif.close();
- }, 5 * $.SECOND);
+ }, 7 * $.SECOND);
},
onUpdate: function(e) {
if (e.detail[404]) {
@@ -8575,7 +8579,7 @@
return arr.splice(0, i);
},
read: $.debounce(50, function(e) {
- var ID, bottom, height, i, post, posts;
+ var ID, height, i, post, posts;
if (d.hidden || !Unread.posts.length) {
return;
@@ -8584,8 +8588,7 @@
posts = Unread.posts;
i = 0;
while (post = posts[i]) {
- bottom = post.nodes.root.getBoundingClientRect().bottom;
- if (bottom < height) {
+ if (post.nodes.root.getBoundingClientRect().bottom < height) {
ID = post.ID;
if (Conf['Mark Quotes of You']) {
if (post.info.yours) {
@@ -8603,10 +8606,8 @@
}
i++;
}
- if (!Conf['Quote Threading']) {
- if (i) {
- posts.splice(0, i);
- }
+ if (i && !Conf['Quote Threading']) {
+ posts.splice(0, i);
}
if (!ID) {
return;
@@ -8631,19 +8632,17 @@
});
}),
setLine: function(force) {
- var post, root;
+ var post;
if (!(d.hidden || force === true)) {
return;
}
- if (post = Unread.posts[0]) {
- root = post.nodes.root;
- if (root !== $('.thread > .replyContainer', root.parentNode)) {
- return $.before(root, Unread.hr);
- }
- } else {
+ if (!(post = Unread.posts[0])) {
return $.rm(Unread.hr);
}
+ if ($.x('preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root)) {
+ return $.before(post.nodes.root, Unread.hr);
+ }
},
update: function(dontrepeat) {
var count;
@@ -8651,17 +8650,18 @@
count = Unread.posts.length;
if (Conf['Unread Count']) {
d.title = "" + (Conf['Quoted Title'] && Unread.postsQuotingYou.length ? '(!) ' : '') + (count || !Conf['Hide Unread Count at (0)'] ? "(" + count + ") " : '') + (g.DEAD ? "/" + g.BOARD + "/ - 404" : "" + Unread.title);
- if (!dontrepeat) {
- setTimeout(function() {
- d.title = '';
- return Unread.update(true);
- }, $.SECOND);
+ if (dontrepeat) {
+ return;
}
+ setTimeout(function() {
+ d.title = '';
+ return Unread.update(true);
+ }, $.SECOND);
}
if (!Conf['Unread Favicon']) {
return;
}
- return Favicon.el.href = g.DEAD ? Unread.postsQuotingYou.length ? Favicon.unreadDeadY : count ? Favicon.unreadDead : Favicon.dead : count ? Unread.postsQuotingYou.length ? Favicon.unreadY : Favicon.unread : Favicon["default"];
+ return Favicon.el.href = g.DEAD ? Unread.postsQuotingYou[0] ? Favicon.unreadDeadY : count ? Favicon.unreadDead : Favicon.dead : count ? Unread.postsQuotingYou[0] ? Favicon.unreadY : Favicon.unread : Favicon["default"];
}
};
@@ -11046,7 +11046,7 @@
}));
Main.logError({
message: 'Chrome Storage API bug',
- error: new Error(chrome.runtime.lastError.message || 'no lastError.message')
+ error: new Error('~')
});
}
return Main.initFeatures();
@@ -11288,6 +11288,7 @@
} catch (_error) {
err = _error;
new Notice('warning', 'Cookies need to be enabled on 4chan for 4chan X to properly function.', 30);
+ Main.disableReports = true;
}
return $.event('4chanXInitFinished');
},
diff --git a/src/General/Header.coffee b/src/General/Header.coffee
index cdbf9ef2f..f584b409b 100755
--- a/src/General/Header.coffee
+++ b/src/General/Header.coffee
@@ -29,7 +29,7 @@ Header =
@linkJustifyToggler = linkJustifyToggler.firstElementChild
@headerToggler = headerToggler.firstElementChild
@footerToggler = footerToggler.firstElementChild
- @shortcutToggler = shortcutToggler.firstElementChild
+ @shortcutToggler = shortcutToggler.firstElementChild
@customNavToggler = customNavToggler.firstElementChild
$.on menuButton, 'click', @menuToggle
@@ -362,7 +362,8 @@ Header =
el = $.el 'span',
innerHTML: """
- Desktop notification permissions are not granted:
+ Desktop notification permissions are not granted.
+ [FAQ]
or
"""
[authorize, disable] = $$ 'button', el
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index 7f48e4eb2..a64de607a 100755
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -29,7 +29,7 @@ Main =
# Track resolution of this bug.
Main.logError
message: 'Chrome Storage API bug'
- error: new Error chrome.runtime.lastError.message or 'no lastError.message'
+ error: new Error '~'
<% } %>
Main.initFeatures()
@@ -237,6 +237,7 @@ Main =
localStorage.getItem '4chan-settings'
catch err
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
+ Main.disableReports = true
$.event '4chanXInitFinished'
diff --git a/src/General/lib/post.class b/src/General/lib/post.class
index ee4391bbf..8224e9941 100755
--- a/src/General/lib/post.class
+++ b/src/General/lib/post.class
@@ -62,6 +62,8 @@ class Post
@kill() if that.isArchived
parseComment: ->
+ # Merge text nodes and remove empty ones.
+ @nodes.comment.normalize()
# Get the comment's text.
#
-> \n
# Remove:
diff --git a/src/Linkification/Linkify.coffee b/src/Linkification/Linkify.coffee
index 382660ac3..784dfe41c 100755
--- a/src/Linkification/Linkify.coffee
+++ b/src/Linkification/Linkify.coffee
@@ -398,4 +398,5 @@ Linkify =
src: "//www.youtube.com/embed/#{a.dataset.uid}#{if a.dataset.option then '#' + a.dataset.option else ''}?wmode=opaque"
title:
api: (uid) -> "https://gdata.youtube.com/feeds/api/videos/#{uid}?alt=json&fields=title/text(),yt:noembed,app:control/yt:state/@reasonCode"
- text: (data) -> data.entry.title.$t
\ No newline at end of file
+ text: (data) -> data.entry.title.$t
+
diff --git a/src/Monitoring/Favicon.coffee b/src/Monitoring/Favicon.coffee
index 8a29c57d2..ea02b3ca2 100755
--- a/src/Monitoring/Favicon.coffee
+++ b/src/Monitoring/Favicon.coffee
@@ -45,6 +45,5 @@ Favicon =
Favicon.unread = Favicon.unreadNSFW
Favicon.unreadY = Favicon.unreadNSFWY
- empty: 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/empty.gif", {encoding: "base64"}) %>'
dead: 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/dead.gif", {encoding: "base64"}) %>'
logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>'
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index d46777ef6..073966965 100755
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -177,7 +177,9 @@ ThreadWatcher =
for threadID, thread of g.BOARD.threads
toggler = $ '.watch-thread-link', thread.OP.nodes.post
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
- $[if watched then 'addClass' else 'rmClass'] toggler, 'watched'
+ helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch']
+ $[helper[0]] toggler, 'watched'
+ toggler.title = "#{helper[1]} Thread"
for refresher in ThreadWatcher.menu.refreshers
refresher()
diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee
index f597614a5..7e5d78b5e 100755
--- a/src/Monitoring/Unread.coffee
+++ b/src/Monitoring/Unread.coffee
@@ -1,6 +1,6 @@
Unread =
init: ->
- return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Favicon']
+ return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Favicon'] and !Conf['Desktop Notifications']
@db = new DataBoard 'lastReadPosts', @sync
@hr = $.el 'hr',
@@ -30,14 +30,14 @@ Unread =
for ID, post of Unread.thread.posts
posts.push post if post.isReply
Unread.addPosts posts
- Unread.scroll() if Conf['Scroll to Last Read Post']
+ Unread.scroll()
scroll: ->
+ return unless Conf['Scroll to Last Read Post']
# Let the header's onload callback handle it.
return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts
- if Unread.posts.length
+ if post = Unread.posts[0]
# Scroll to a non-hidden, non-OP post that's before the first unread post.
- post = Unread.posts[0]
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
break unless (post = Get.postFromRoot root).isHidden
return unless root
@@ -48,11 +48,8 @@ Unread =
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes
onload = -> Header.scrollToPost root if checkPosition root
checkPosition = (target) ->
- # Don't scroll to the target if
- # - it's visible.
- # - we've scrolled past it.
- {top, height} = target.getBoundingClientRect()
- top + height - doc.clientHeight > 0
+ # Scroll to the target unless we scrolled past it.
+ target.getBoundingClientRect().bottom > doc.clientHeight
# Prevent the browser to scroll back to
# the previous scroll location on page load.
$.on window, 'load', onload
@@ -66,7 +63,7 @@ Unread =
Unread.lastReadPost = lastReadPost
Unread.readArray Unread.posts
Unread.readArray Unread.postsQuotingYou
- Unread.setLine()
+ Unread.setLine() if Conf['Unread Line']
Unread.update()
addPosts: (posts) ->
@@ -93,7 +90,8 @@ Unread =
for quotelink in post.nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
Unread.postsQuotingYou.push post
Unread.openNotification post
- return
+ return
+
openNotification: (post) ->
return unless Header.areNotificationsEnabled
name = if Conf['Anonymize']
@@ -108,7 +106,7 @@ Unread =
window.focus()
setTimeout ->
notif.close()
- , 5 * $.SECOND
+ , 7 * $.SECOND
onUpdate: (e) ->
if e.detail[404]
@@ -138,8 +136,7 @@ Unread =
i = 0
while post = posts[i]
- {bottom} = post.nodes.root.getBoundingClientRect()
- if bottom < height # post is completely read
+ if post.nodes.root.getBoundingClientRect().bottom < height # post is not completely read
{ID} = post
if Conf['Mark Quotes of You']
if post.info.yours
@@ -152,9 +149,8 @@ Unread =
break
i++
- unless Conf['Quote Threading']
- if i
- posts.splice 0, i
+ if i and !Conf['Quote Threading']
+ posts.splice 0, i
return unless ID
@@ -172,12 +168,9 @@ Unread =
setLine: (force) ->
return unless d.hidden or force is true
- if post = Unread.posts[0]
- {root} = post.nodes
- if root isnt $ '.thread > .replyContainer', root.parentNode # not the first reply
- $.before root, Unread.hr
- else
- $.rm Unread.hr
+ return $.rm Unread.hr unless post = Unread.posts[0]
+ if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root # not the first reply
+ $.before post.nodes.root, Unread.hr
update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
count = Unread.posts.length
@@ -189,18 +182,18 @@ Unread =
# crbug.com/124381
# Call it one second later,
# but don't display outdated unread count.
- unless dontrepeat
- setTimeout ->
- d.title = ''
- Unread.update true
- , $.SECOND
+ return if dontrepeat
+ setTimeout ->
+ d.title = ''
+ Unread.update true
+ , $.SECOND
<% } %>
return unless Conf['Unread Favicon']
Favicon.el.href =
if g.DEAD
- if Unread.postsQuotingYou.length
+ if Unread.postsQuotingYou[0]
Favicon.unreadDeadY
else if count
Favicon.unreadDead
@@ -208,7 +201,7 @@ Unread =
Favicon.dead
else
if count
- if Unread.postsQuotingYou.length
+ if Unread.postsQuotingYou[0]
Favicon.unreadY
else
Favicon.unread
diff --git a/src/Posting/QuickReply.coffee b/src/Posting/QuickReply.coffee
index c4c0ad45b..8cda679e2 100755
--- a/src/Posting/QuickReply.coffee
+++ b/src/Posting/QuickReply.coffee
@@ -165,7 +165,7 @@ QR =
setTimeout ->
notif.onclose = null
notif.close()
- , 5 * $.SECOND
+ , 7 * $.SECOND
<% } %>
notifications: []
@@ -448,69 +448,56 @@ QR =
return unless e.dataTransfer.files.length
e.preventDefault()
QR.open()
- QR.fileInput e.dataTransfer.files
+ QR.handleFiles e.dataTransfer.files
$.addClass QR.nodes.el, 'dump'
paste: (e) ->
files = []
- for item in e.clipboardData.items
- if item.kind is 'file'
- blob = item.getAsFile()
- blob.name = 'file'
- blob.name += '.' + blob.type.split('/')[1] if blob.type
- files.push blob
+ for item in e.clipboardData.items when item.kind is 'file'
+ blob = item.getAsFile()
+ blob.name = 'file'
+ blob.name += '.' + blob.type.split('/')[1] if blob.type
+ files.push blob
return unless files.length
QR.open()
- QR.fileInput files
-
- openFileInput: (e) ->
- e.stopPropagation()
- if e.shiftKey and e.type is 'click'
- return QR.selected.rmFile()
- if e.ctrlKey and e.type is 'click'
- $.addClass QR.nodes.filename, 'edit'
- QR.nodes.filename.focus()
- return $.on QR.nodes.filename, 'blur', -> $.rmClass QR.nodes.filename, 'edit'
- return if e.target.nodeName is 'INPUT' or (e.keyCode and not [32, 13].contains e.keyCode) or e.ctrlKey
- e.preventDefault()
- QR.nodes.fileInput.click()
-
- fileInput: (files) ->
- if @ instanceof Element # file input, revert to "files instanceof Event" after a Pale Moon update
- files = [@files...]
- QR.nodes.fileInput.value = null # Don't hold the files from being modified on windows
- {length} = files
- return unless length
- max = QR.nodes.fileInput.max
- QR.cleanNotifications()
- # Set or change current post's file.
- if length is 1
- file = files[0]
- if /^text/.test file.type
- QR.selected.pasteText file
- else if file.size > max
- QR.error "File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
- else unless QR.mimeTypes.contains file.type
- QR.error 'Unsupported file type.'
- else
- QR.selected.setFile file
- return
- # Create new posts with these files.
- for file in files
- if /^text/.test file.type
- if (post = QR.posts[QR.posts.length - 1]).com
- post = new QR.post()
- post.pasteText file
- else if file.size > max
- QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
- else unless QR.mimeTypes.contains file.type
- QR.error "#{file.name}: Unsupported file type."
- else
- if (post = QR.posts[QR.posts.length - 1]).file
- post = new QR.post()
- post.setFile file
+ QR.handleFiles files
$.addClass QR.nodes.el, 'dump'
+ handleFiles: (files) ->
+ if @ isnt QR # file input
+ files = [@files...]
+ @value = null
+ return unless files.length
+ max = QR.nodes.fileInput.max
+ isSingle = files.length is 1
+ QR.cleanNotifications()
+ for file in files
+ QR.handleFile file, isSingle, max
+ $.addClass QR.nodes.el, 'dump' unless isSingle
+
+ handleFile: (file, isSingle, max) ->
+ if file.size > max
+ QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
+ return
+ else unless QR.mimeTypes.contains file.type
+ unless /^text/.test file.type
+ QR.error "#{file.name}: Unsupported file type."
+ return
+ if isSingle
+ post = QR.selected
+ else if (post = QR.posts[QR.posts.length - 1]).com
+ post = new QR.post()
+ post.pasteText file
+ return
+ if isSingle
+ post = QR.selected
+ else if (post = QR.posts[QR.posts.length - 1]).file
+ post = new QR.post()
+ post.setFile file
+
+ openFileInput: ->
+ QR.nodes.fileInput.click()
+
posts: []
post: class
@@ -748,7 +735,9 @@ QR =
@nodes.span.textContent = @com
reader.readAsText file
- dragStart: -> $.addClass @, 'drag'
+ dragStart: (e) ->
+ e.dataTransfer.setDragImage @, e.layerX, e.layerY
+ $.addClass @, 'drag'
dragEnd: -> $.rmClass @, 'drag'
dragEnter: -> $.addClass @, 'over'
dragLeave: -> $.rmClass @, 'over'
@@ -987,7 +976,7 @@ QR =
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
- $.on nodes.fileInput, 'change', QR.fileInput
+ $.on nodes.fileInput, 'change', QR.handleFiles
# save selected post's data
items = ['name', 'email', 'sub', 'com', 'filename']
i = 0
@@ -1213,7 +1202,19 @@ QR =
}
# Enable auto-posting if we have stuff to post, disable it otherwise.
- QR.cooldown.auto = QR.posts.length > 1 and isReply
+ postsCount = QR.posts.length
+ QR.cooldown.auto = postsCount > 1 and isReply
+ if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount
+ notif = new Notification 'Quick reply warning',
+ body: "You are running low on cached captchas. Cache count: #{captchasCount}."
+ icon: Favicon.logo
+ notif.onclick = ->
+ QR.open()
+ QR.captcha.nodes.input.focus()
+ window.focus()
+ setTimeout ->
+ notif.close()
+ , 7 * $.SECOND
unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close()