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()