From 13563ffcda5bc72a871e8183e71a296608cdac74 Mon Sep 17 00:00:00 2001 From: Zixaphir Date: Fri, 9 Jan 2015 23:25:47 -0700 Subject: [PATCH] Quick Reply --- builds/appchan-x.user.js | 403 +++++++--------------- builds/crx/script.js | 402 +++++++-------------- src/General/Navigate.coffee | 1 - src/General/html/Features/QuickReply.html | 4 +- src/Posting/QR.captcha.coffee | 173 ---------- src/Posting/QR.coffee | 172 +++++---- 6 files changed, 344 insertions(+), 811 deletions(-) delete mode 100644 src/Posting/QR.captcha.coffee diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index 046600a24..eff44882e 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -9101,10 +9101,19 @@ QR = { mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'], init: function() { - var con, sc; + var con, noscript, sc; this.db = new DataBoard('yourPosts'); this.posts = []; + if (g.VIEW === 'archive') { + return; + } + $.globalEval('document.documentElement.dataset.jsEnabled = true;'); + noscript = Conf['Force Noscript Captcha'] || !doc.dataset.jsEnabled; + this.captcha = Captcha[noscript ? 'noscript' : 'v2']; $.on(d, '4chanXInitFinished', this.initReady); + window.addEventListener('focus', this.focus, true); + window.addEventListener('blur', this.focus, true); + $.on(d, 'click', this.focus); Post.callbacks.push({ name: 'Quick Reply', cb: this.node @@ -9154,6 +9163,7 @@ if (!QR.postingIsEnabled) { return; } + $.on(d, 'paste', QR.paste); $.on(d, 'dragover', QR.dragOver); $.on(d, 'drop', QR.dropFile); $.on(d, 'dragstart dragend', QR.drag); @@ -9163,12 +9173,17 @@ return; } QR.open(); - if (Conf['Auto-Hide QR']) { + if (Conf['Auto Hide QR']) { return QR.hide(); } }, statusCheck: function() { - if (g.DEAD) { + var thread; + if (!QR.nodes) { + return; + } + thread = QR.posts[0].thread; + if (thread !== 'new' && g.threads["" + g.BOARD + "." + thread].isDead) { return QR.abort(); } else { return QR.status(); @@ -9196,9 +9211,11 @@ open: function() { var err; if (QR.nodes) { + if (QR.nodes.el.hidden) { + QR.captcha.setup(); + } QR.nodes.el.hidden = false; QR.unhide(); - QR.captcha.setup(); return; } try { @@ -9235,14 +9252,34 @@ QR.status(); return QR.captcha.destroy(); }, - focusin: function() { - if ($.hasClass(QR.nodes.el, 'autohide') && !$.hasClass(QR.nodes.el, 'focus')) { - QR.captcha.setup(); - } - return $.addClass(QR.nodes.el, 'focus'); + focus: function() { + return $.queueTask(function() { + var focus; + if (!QR.nodes) { + return; + } + if (!$$('.goog-bubble-content > iframe').some(function(el) { + return el.getBoundingClientRect().top >= 0; + })) { + focus = d.activeElement && QR.nodes.el.contains(d.activeElement); + $[focus ? 'addClass' : 'rmClass'](QR.nodes.el, 'focus'); + } + if (typeof chrome !== "undefined" && chrome !== null) { + if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') { + QR.scrollY = window.scrollY; + return $.on(d, 'scroll', QR.scrollLock); + } else { + return $.off(d, 'scroll', QR.scrollLock); + } + } + }); }, - focusout: function() { - return $.rmClass(QR.nodes.el, 'focus'); + scrollLock: function(e) { + if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') { + return window.scroll(window.scrollX, QR.scrollY); + } else { + return $.off(d, 'scroll', QR.scrollLock); + } }, hide: function() { d.activeElement.blur(); @@ -9271,8 +9308,10 @@ } if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) { QR.captcha.setup(true); + QR.captcha.notify(el); + } else { + QR.notify(el); } - QR.notify(el); if (d.hidden) { return alert(el.textContent); } @@ -9328,7 +9367,7 @@ } sel = d.getSelection(); post = Get.postFromNode(this); - text = ">>" + post + "\n"; + text = post.board.ID === g.BOARD.ID ? ">>" + post + "\n" : ">>>/" + post.board + "/" + post + "\n"; if (sel.toString().trim() && post === Get.postFromNode(sel.anchorNode)) { range = sel.getRangeAt(0); frag = range.cloneContents(); @@ -9557,16 +9596,17 @@ } list.value = g.VIEW === 'thread' ? g.THREADID : 'new'; if ($.hasClass(list, 'riced')) { - return list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent; + list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent; } + return (g.VIEW === 'thread' ? $.addClass : $.rmClass)(QR.nodes.el, 'reply-to-thread'); }, dialog: function() { - var dialog, elm, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; + var dialog, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; QR.nodes = nodes = { el: dialog = UI.dialog('qr', 'top:0;right:0;') }; $.extend(dialog, { - innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" + innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" }); setNode = function(name, query) { return nodes[name] = $(query, dialog); @@ -9606,6 +9646,14 @@ QR.max_size_video = 3145728; QR.max_width_video = QR.max_height_video = 2048; QR.max_duration_video = 120; + if (Conf['Show New Thread Option in Threads']) { + $.addClass(QR.nodes.el, 'show-new-thread-option'); + } + if (Conf['Show Name and Subject']) { + $.addClass(QR.nodes.name, 'force-show'); + $.addClass(QR.nodes.sub, 'force-show'); + QR.nodes.email.placeholder = 'E-mail'; + } QR.forcedAnon = !!$('form[name="post"] input[name="name"][type="hidden"]'); if (QR.forcedAnon) { $.addClass(QR.nodes.el, 'forced-anon'); @@ -9622,20 +9670,15 @@ } if (g.BOARD.ID === 'f' && g.VIEW !== 'thread') { nodes.flashTag = $.el('select', { - name: 'filetag', - innerHTML: "\n\n\n\n\n\n" - }); + name: 'filetag' + }, $.extend(nodes.flashTag, { + innerHTML: "" + })); nodes.flashTag.dataset["default"] = '4'; $.add(nodes.form, nodes.flashTag); } QR.flagsInput(); $.on(nodes.filename.parentNode, 'click keydown', QR.openFileInput); - items = $$('*', QR.nodes.el); - i = 0; - while (elm = items[i++]) { - $.on(elm, 'blur', QR.focusout); - $.on(elm, 'focus', QR.focusin); - } $.on(nodes.autohide, 'change', QR.toggleHide); $.on(nodes.close, 'click', QR.close); $.on(nodes.dumpButton, 'click', function() { @@ -9664,7 +9707,7 @@ while (name = items[i++]) { $.on(nodes[name], 'mouseover', QR.mouseover); } - items = ['name', 'email', 'sub', 'com', 'filename', 'flag']; + items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']; i = 0; save = function() { return QR.selected.save(this); @@ -9734,25 +9777,18 @@ return select; }, flagsInput: function() { - var flag, nodes; + var nodes; nodes = QR.nodes; if (!nodes) { return; } if (nodes.flag) { $.rm(nodes.flag); - delete nodes.flag; - } - if (g.BOARD.ID === 'pol') { - flag = QR.flags(); - flag.dataset.name = 'flag'; - flag.dataset["default"] = '0'; - nodes.flag = flag; - return $.add(nodes.form, flag); + return delete nodes.flag; } }, submit: function(e) { - var err, extra, filetag, formData, options, post, response, textOnly, thread, threadID; + var captcha, cb, err, extra, filetag, formData, options, post, textOnly, thread, threadID; if (e != null) { e.preventDefault(); } @@ -9787,8 +9823,8 @@ err = 'Max limit of image replies has been reached.'; } if (QR.captcha.isEnabled && !err) { - response = QR.captcha.getOne(); - if (!response) { + captcha = QR.captcha.getOne(); + if (!captcha) { err = 'No valid captcha.'; } } @@ -9819,8 +9855,7 @@ flag: post.flag, textonly: textOnly, mode: 'regist', - pwd: QR.persona.pwd, - 'g-recaptcha-response': response + pwd: QR.persona.pwd }; options = { responseType: 'document', @@ -9832,7 +9867,7 @@ QR.cooldown.auto = false; QR.status(); return QR.error($.el('span', { - innerHTML: "Connection error. You may have been banned.\n[?]" + innerHTML: "4chan X encountered an error while posting. [Banned?] [More info]" })); } }; @@ -9851,9 +9886,33 @@ } } }; - QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra); - QR.req.uploadStartTime = Date.now(); - QR.req.progress = '...'; + cb = function(response) { + if (response != null) { + extra.form.append('g-recaptcha-response', response); + } + QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra); + return QR.req.progress = '...'; + }; + if (typeof captcha === 'function') { + QR.req = { + progress: '...', + abort: function() { + return cb = null; + } + }; + captcha(function(response) { + if (response) { + return typeof cb === "function" ? cb(response) : void 0; + } else { + delete QR.req; + post.unlock(); + QR.cooldown.auto = !!QR.captcha.captchas.length; + return QR.status(); + } + }); + } else { + cb(captcha); + } return QR.status(); }, response: function() { @@ -9865,8 +9924,10 @@ resDoc = req.response; if (ban = $('.banType', resDoc)) { board = $('.board', resDoc).innerHTML; - err = $.el('span', { - innerHTML: ban.textContent.toLowerCase() === 'banned' ? "You are banned on " + board + "! ;_;
\nClick here to see the reason." : "You were issued a warning on " + board + " as " + ($('.nameBlock', resDoc).innerHTML) + ".
\nReason: " + ($('.reason', resDoc).innerHTML) + err = $.el('span', ban.textContent.toLowerCase() === 'banned' ? { + innerHTML: "You are banned on " + $(".board", resDoc).innerHTML + "! ;_;
Click here to see the reason." + } : { + innerHTML: "You were issued a warning on " + $(".board", resDoc).innerHTML + " as " + $(".nameBlock", resDoc).innerHTML + ".
Reason: " + $(".reason", resDoc).innerHTML }); } else if (err = resDoc.getElementById('errmsg')) { if ((_ref = $('a', err)) != null) { @@ -9885,14 +9946,11 @@ err = 'This CAPTCHA is no longer valid because it has expired.'; } QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false; - QR.cooldown.set({ - delay: 2 - }); - } else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) { + QR.cooldown.addDelay(post, 2); + } else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i)) && !/duplicate/i.test(err.textContent)) { QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true; - QR.cooldown.set({ - delay: m[1] - }); + QR.cooldown.addDelay(post, +m[1]); + QR.captcha.setup(d.activeElement === QR.nodes.status); } else { QR.cooldown.auto = false; } @@ -9936,7 +9994,8 @@ }); notif.onclick = function() { QR.open(); - return window.focus(); + window.focus(); + return QR.captcha.setup(true); }; notif.onshow = function() { return setTimeout(function() { @@ -9944,21 +10003,16 @@ }, 7 * $.SECOND); }; } - if (!(Conf['Persistent QR'] || QR.cooldown.auto)) { + if (!(Conf['Persistent QR'] || postsCount)) { QR.close(); } else { post.rm(); - QR.captcha.setup(true); + QR.captcha.setup(d.activeElement === QR.nodes.status); } - QR.cooldown.set({ - req: req, - post: post, - isReply: isReply, - threadID: threadID - }); - URL = threadID === postID ? Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? Build.path(g.BOARD.ID, threadID, postID) : void 0; + QR.cooldown.add(req.uploadEndTime, threadID, postID); + URL = threadID === postID ? window.location.origin + Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? window.location.origin + Build.path(g.BOARD.ID, threadID, postID) : void 0; if (URL) { - if (Conf['Open Post in New Tab']) { + if (Conf['Open Post in New Tab'] || postsCount) { $.open(URL); } else { window.location = URL; @@ -9998,226 +10052,6 @@ } }; - QR.captcha = { - init: function() { - var counter, root; - if (d.cookie.indexOf('pass_enabled=1') >= 0) { - return; - } - if (!(this.isEnabled = !!$.id('g-recaptcha'))) { - return; - } - this.captchas = []; - $.get('captchas', [], function(_arg) { - var captchas; - captchas = _arg.captchas; - return QR.captcha.sync(captchas); - }); - $.sync('captchas', this.sync.bind(this)); - root = $.el('div', { - className: 'captcha-root' - }); - $.extend(root, { - innerHTML: "
" - }); - counter = $('.captcha-counter > a', root); - this.nodes = { - root: root, - counter: counter - }; - this.count(); - $.addClass(QR.nodes.el, 'has-captcha'); - $.after(QR.nodes.com.parentNode, root); - $.on(counter, 'click', this.toggle.bind(this)); - return $.on(window, 'captcha:success', (function(_this) { - return function() { - return $.queueTask(function() { - return _this.save(false); - }); - }; - })(this)); - }, - shouldFocus: false, - timeouts: {}, - postsCount: 0, - needed: function() { - var captchaCount; - captchaCount = this.captchas.length; - if (this.nodes.container && !this.timeouts.destroy) { - captchaCount++; - } - this.postsCount = QR.posts.length; - if (this.postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { - this.postsCount = 0; - } - return captchaCount < this.postsCount; - }, - onPostChange: function() { - if (this.postsCount === 0) { - this.setup(); - } - if (QR.posts.length === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { - return this.postsCount = 0; - } - }, - toggle: function() { - if (this.nodes.container && !this.timeouts.destroy) { - return this.destroy(); - } else { - return this.setup(true, true); - } - }, - setup: function(focus, force) { - if (!(this.isEnabled && (this.needed() || force))) { - return; - } - $.addClass(QR.nodes.el, 'captcha-open'); - if (focus) { - this.shouldFocus = true; - } - if (this.timeouts.destroy) { - clearTimeout(this.timeouts.destroy); - delete this.timeouts.destroy; - return this.reload(); - } - if (this.nodes.container) { - return; - } - this.nodes.container = $.el('div', { - className: 'captcha-container' - }); - $.prepend(this.nodes.root, this.nodes.container); - new MutationObserver(this.afterSetup.bind(this)).observe(this.nodes.container, { - childList: true, - subtree: true - }); - return $.globalEval('(function() {\n function render() {\n var container = document.querySelector("#qr .captcha-container");\n container.dataset.widgetID = window.grecaptcha.render(container, {\n sitekey: \'6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\',\n theme: document.documentElement.classList.contains(\'tomorrow\') ? \'dark\' : \'light\',\n callback: function(response) {\n window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));\n }\n });\n }\n if (window.grecaptcha) {\n render();\n } else {\n var cbNative = window.onRecaptchaLoaded;\n window.onRecaptchaLoaded = function() {\n render();\n cbNative();\n }\n }\n})();'); - }, - afterSetup: function(mutations) { - var iframe, mutation, node, textarea, _i, _j, _len, _len1, _ref; - for (_i = 0, _len = mutations.length; _i < _len; _i++) { - mutation = mutations[_i]; - _ref = mutation.addedNodes; - for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { - node = _ref[_j]; - if (iframe = $.x('./descendant-or-self::iframe', node)) { - this.setupIFrame(iframe); - } - if (textarea = $.x('./descendant-or-self::textarea', node)) { - this.setupTextArea(textarea); - } - } - } - }, - setupIFrame: function(iframe) { - this.setupTime = Date.now(); - if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { - QR.nodes.el.style.top = null; - QR.nodes.el.style.bottom = '0px'; - } - if (this.shouldFocus) { - iframe.focus(); - } - return this.shouldFocus = false; - }, - setupTextArea: function(textarea) { - return $.one(textarea, 'input', (function(_this) { - return function() { - return _this.save(true); - }; - })(this)); - }, - destroy: function() { - if (!this.isEnabled) { - return; - } - delete this.timeouts.destroy; - $.rmClass(QR.nodes.el, 'captcha-open'); - if (this.nodes.container) { - $.rm(this.nodes.container); - } - return delete this.nodes.container; - }, - sync: function(captchas) { - if (captchas == null) { - captchas = []; - } - this.captchas = captchas; - this.clear(); - return this.count(); - }, - getOne: function() { - var captcha; - this.clear(); - if (captcha = this.captchas.shift()) { - this.count(); - $.set('captchas', this.captchas); - return captcha.response; - } else { - return null; - } - }, - save: function(pasted) { - var reload, _base; - $.forceSync('captchas'); - reload = (QR.cooldown.auto || Conf['Post on Captcha Completion']) && this.needed(); - this.captchas.push({ - response: $('textarea', this.nodes.container).value, - timeout: (pasted ? this.setupTime : Date.now()) + 2 * $.MINUTE - }); - this.count(); - $.set('captchas', this.captchas); - if (reload) { - this.shouldFocus = true; - this.reload(); - } else { - if (pasted) { - this.destroy(); - } else { - if ((_base = this.timeouts).destroy == null) { - _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND); - } - } - QR.nodes.status.focus(); - } - if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) { - return QR.submit(); - } - }, - clear: function() { - var captcha, i, now, _i, _len, _ref; - if (!this.captchas.length) { - return; - } - $.forceSync('captchas'); - now = Date.now(); - _ref = this.captchas; - for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { - captcha = _ref[i]; - if (captcha.timeout > now) { - break; - } - } - if (!i) { - return; - } - this.captchas = this.captchas.slice(i); - this.count(); - $.set('captchas', this.captchas); - return this.setup(true); - }, - count: function() { - this.nodes.counter.textContent = "Captchas: " + this.captchas.length; - clearTimeout(this.timeouts.clear); - if (this.captchas.length) { - return this.timeouts.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); - } - }, - reload: function(focus) { - return $.globalEval('(function() {\n var container = document.querySelector("#qr .captcha-container");\n window.grecaptcha.reset(container.dataset.widgetID);\n})();'); - } - }; - QR.cooldown = { init: function() { var key, setTimers, type; @@ -18351,7 +18185,6 @@ } }, updateContext: function(view) { - g.DEAD = false; if (view === 'thread') { g.THREADID = +window.location.pathname.split('/')[3]; } diff --git a/builds/crx/script.js b/builds/crx/script.js index 481dcb430..a519df541 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -9146,10 +9146,19 @@ QR = { mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'], init: function() { - var con, sc; + var con, noscript, sc; this.db = new DataBoard('yourPosts'); this.posts = []; + if (g.VIEW === 'archive') { + return; + } + $.globalEval('document.documentElement.dataset.jsEnabled = true;'); + noscript = Conf['Force Noscript Captcha'] || !doc.dataset.jsEnabled; + this.captcha = Captcha[noscript ? 'noscript' : 'v2']; $.on(d, '4chanXInitFinished', this.initReady); + window.addEventListener('focus', this.focus, true); + window.addEventListener('blur', this.focus, true); + $.on(d, 'click', this.focus); Post.callbacks.push({ name: 'Quick Reply', cb: this.node @@ -9209,12 +9218,17 @@ return; } QR.open(); - if (Conf['Auto-Hide QR']) { + if (Conf['Auto Hide QR']) { return QR.hide(); } }, statusCheck: function() { - if (g.DEAD) { + var thread; + if (!QR.nodes) { + return; + } + thread = QR.posts[0].thread; + if (thread !== 'new' && g.threads["" + g.BOARD + "." + thread].isDead) { return QR.abort(); } else { return QR.status(); @@ -9242,9 +9256,11 @@ open: function() { var err; if (QR.nodes) { + if (QR.nodes.el.hidden) { + QR.captcha.setup(); + } QR.nodes.el.hidden = false; QR.unhide(); - QR.captcha.setup(); return; } try { @@ -9281,14 +9297,34 @@ QR.status(); return QR.captcha.destroy(); }, - focusin: function() { - if ($.hasClass(QR.nodes.el, 'autohide') && !$.hasClass(QR.nodes.el, 'focus')) { - QR.captcha.setup(); - } - return $.addClass(QR.nodes.el, 'focus'); + focus: function() { + return $.queueTask(function() { + var focus; + if (!QR.nodes) { + return; + } + if (!$$('.goog-bubble-content > iframe').some(function(el) { + return el.getBoundingClientRect().top >= 0; + })) { + focus = d.activeElement && QR.nodes.el.contains(d.activeElement); + $[focus ? 'addClass' : 'rmClass'](QR.nodes.el, 'focus'); + } + if (typeof chrome !== "undefined" && chrome !== null) { + if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') { + QR.scrollY = window.scrollY; + return $.on(d, 'scroll', QR.scrollLock); + } else { + return $.off(d, 'scroll', QR.scrollLock); + } + } + }); }, - focusout: function() { - return $.rmClass(QR.nodes.el, 'focus'); + scrollLock: function(e) { + if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') { + return window.scroll(window.scrollX, QR.scrollY); + } else { + return $.off(d, 'scroll', QR.scrollLock); + } }, hide: function() { d.activeElement.blur(); @@ -9317,8 +9353,10 @@ } if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) { QR.captcha.setup(true); + QR.captcha.notify(el); + } else { + QR.notify(el); } - QR.notify(el); if (d.hidden) { return alert(el.textContent); } @@ -9383,7 +9421,7 @@ } sel = d.getSelection(); post = Get.postFromNode(this); - text = ">>" + post + "\n"; + text = post.board.ID === g.BOARD.ID ? ">>" + post + "\n" : ">>>/" + post.board + "/" + post + "\n"; if (sel.toString().trim() && post === Get.postFromNode(sel.anchorNode)) { range = sel.getRangeAt(0); frag = range.cloneContents(); @@ -9612,16 +9650,17 @@ } list.value = g.VIEW === 'thread' ? g.THREADID : 'new'; if ($.hasClass(list, 'riced')) { - return list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent; + list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent; } + return (g.VIEW === 'thread' ? $.addClass : $.rmClass)(QR.nodes.el, 'reply-to-thread'); }, dialog: function() { - var dialog, elm, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; + var dialog, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; QR.nodes = nodes = { el: dialog = UI.dialog('qr', 'top:0;right:0;') }; $.extend(dialog, { - innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" + innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" }); setNode = function(name, query) { return nodes[name] = $(query, dialog); @@ -9661,6 +9700,14 @@ QR.max_size_video = 3145728; QR.max_width_video = QR.max_height_video = 2048; QR.max_duration_video = 120; + if (Conf['Show New Thread Option in Threads']) { + $.addClass(QR.nodes.el, 'show-new-thread-option'); + } + if (Conf['Show Name and Subject']) { + $.addClass(QR.nodes.name, 'force-show'); + $.addClass(QR.nodes.sub, 'force-show'); + QR.nodes.email.placeholder = 'E-mail'; + } QR.forcedAnon = !!$('form[name="post"] input[name="name"][type="hidden"]'); if (QR.forcedAnon) { $.addClass(QR.nodes.el, 'forced-anon'); @@ -9677,20 +9724,15 @@ } if (g.BOARD.ID === 'f' && g.VIEW !== 'thread') { nodes.flashTag = $.el('select', { - name: 'filetag', - innerHTML: "\n\n\n\n\n\n" - }); + name: 'filetag' + }, $.extend(nodes.flashTag, { + innerHTML: "" + })); nodes.flashTag.dataset["default"] = '4'; $.add(nodes.form, nodes.flashTag); } QR.flagsInput(); $.on(nodes.filename.parentNode, 'click keydown', QR.openFileInput); - items = $$('*', QR.nodes.el); - i = 0; - while (elm = items[i++]) { - $.on(elm, 'blur', QR.focusout); - $.on(elm, 'focus', QR.focusin); - } $.on(nodes.autohide, 'change', QR.toggleHide); $.on(nodes.close, 'click', QR.close); $.on(nodes.dumpButton, 'click', function() { @@ -9719,7 +9761,7 @@ while (name = items[i++]) { $.on(nodes[name], 'mouseover', QR.mouseover); } - items = ['name', 'email', 'sub', 'com', 'filename', 'flag']; + items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']; i = 0; save = function() { return QR.selected.save(this); @@ -9778,25 +9820,18 @@ return select; }, flagsInput: function() { - var flag, nodes; + var nodes; nodes = QR.nodes; if (!nodes) { return; } if (nodes.flag) { $.rm(nodes.flag); - delete nodes.flag; - } - if (g.BOARD.ID === 'pol') { - flag = QR.flags(); - flag.dataset.name = 'flag'; - flag.dataset["default"] = '0'; - nodes.flag = flag; - return $.add(nodes.form, flag); + return delete nodes.flag; } }, submit: function(e) { - var err, extra, filetag, formData, options, post, response, textOnly, thread, threadID; + var captcha, cb, err, extra, filetag, formData, options, post, textOnly, thread, threadID; if (e != null) { e.preventDefault(); } @@ -9831,8 +9866,8 @@ err = 'Max limit of image replies has been reached.'; } if (QR.captcha.isEnabled && !err) { - response = QR.captcha.getOne(); - if (!response) { + captcha = QR.captcha.getOne(); + if (!captcha) { err = 'No valid captcha.'; } } @@ -9863,8 +9898,7 @@ flag: post.flag, textonly: textOnly, mode: 'regist', - pwd: QR.persona.pwd, - 'g-recaptcha-response': response + pwd: QR.persona.pwd }; options = { responseType: 'document', @@ -9876,7 +9910,7 @@ QR.cooldown.auto = false; QR.status(); return QR.error($.el('span', { - innerHTML: "Connection error. You may have been banned.\n[?]" + innerHTML: "4chan X encountered an error while posting. [Banned?] [More info]" })); } }; @@ -9895,9 +9929,33 @@ } } }; - QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra); - QR.req.uploadStartTime = Date.now(); - QR.req.progress = '...'; + cb = function(response) { + if (response != null) { + extra.form.append('g-recaptcha-response', response); + } + QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra); + return QR.req.progress = '...'; + }; + if (typeof captcha === 'function') { + QR.req = { + progress: '...', + abort: function() { + return cb = null; + } + }; + captcha(function(response) { + if (response) { + return typeof cb === "function" ? cb(response) : void 0; + } else { + delete QR.req; + post.unlock(); + QR.cooldown.auto = !!QR.captcha.captchas.length; + return QR.status(); + } + }); + } else { + cb(captcha); + } return QR.status(); }, response: function() { @@ -9909,8 +9967,10 @@ resDoc = req.response; if (ban = $('.banType', resDoc)) { board = $('.board', resDoc).innerHTML; - err = $.el('span', { - innerHTML: ban.textContent.toLowerCase() === 'banned' ? "You are banned on " + board + "! ;_;
\nClick here to see the reason." : "You were issued a warning on " + board + " as " + ($('.nameBlock', resDoc).innerHTML) + ".
\nReason: " + ($('.reason', resDoc).innerHTML) + err = $.el('span', ban.textContent.toLowerCase() === 'banned' ? { + innerHTML: "You are banned on " + $(".board", resDoc).innerHTML + "! ;_;
Click here to see the reason." + } : { + innerHTML: "You were issued a warning on " + $(".board", resDoc).innerHTML + " as " + $(".nameBlock", resDoc).innerHTML + ".
Reason: " + $(".reason", resDoc).innerHTML }); } else if (err = resDoc.getElementById('errmsg')) { if ((_ref = $('a', err)) != null) { @@ -9929,14 +9989,11 @@ err = 'This CAPTCHA is no longer valid because it has expired.'; } QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false; - QR.cooldown.set({ - delay: 2 - }); - } else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) { + QR.cooldown.addDelay(post, 2); + } else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i)) && !/duplicate/i.test(err.textContent)) { QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true; - QR.cooldown.set({ - delay: m[1] - }); + QR.cooldown.addDelay(post, +m[1]); + QR.captcha.setup(d.activeElement === QR.nodes.status); } else { QR.cooldown.auto = false; } @@ -9980,7 +10037,8 @@ }); notif.onclick = function() { QR.open(); - return window.focus(); + window.focus(); + return QR.captcha.setup(true); }; notif.onshow = function() { return setTimeout(function() { @@ -9988,21 +10046,16 @@ }, 7 * $.SECOND); }; } - if (!(Conf['Persistent QR'] || QR.cooldown.auto)) { + if (!(Conf['Persistent QR'] || postsCount)) { QR.close(); } else { post.rm(); - QR.captcha.setup(true); + QR.captcha.setup(d.activeElement === QR.nodes.status); } - QR.cooldown.set({ - req: req, - post: post, - isReply: isReply, - threadID: threadID - }); - URL = threadID === postID ? Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? Build.path(g.BOARD.ID, threadID, postID) : void 0; + QR.cooldown.add(req.uploadEndTime, threadID, postID); + URL = threadID === postID ? window.location.origin + Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? window.location.origin + Build.path(g.BOARD.ID, threadID, postID) : void 0; if (URL) { - if (Conf['Open Post in New Tab']) { + if (Conf['Open Post in New Tab'] || postsCount) { $.open(URL); } else { window.location = URL; @@ -10042,226 +10095,6 @@ } }; - QR.captcha = { - init: function() { - var counter, root; - if (d.cookie.indexOf('pass_enabled=1') >= 0) { - return; - } - if (!(this.isEnabled = !!$.id('g-recaptcha'))) { - return; - } - this.captchas = []; - $.get('captchas', [], function(_arg) { - var captchas; - captchas = _arg.captchas; - return QR.captcha.sync(captchas); - }); - $.sync('captchas', this.sync.bind(this)); - root = $.el('div', { - className: 'captcha-root' - }); - $.extend(root, { - innerHTML: "
" - }); - counter = $('.captcha-counter > a', root); - this.nodes = { - root: root, - counter: counter - }; - this.count(); - $.addClass(QR.nodes.el, 'has-captcha'); - $.after(QR.nodes.com.parentNode, root); - $.on(counter, 'click', this.toggle.bind(this)); - return $.on(window, 'captcha:success', (function(_this) { - return function() { - return $.queueTask(function() { - return _this.save(false); - }); - }; - })(this)); - }, - shouldFocus: false, - timeouts: {}, - postsCount: 0, - needed: function() { - var captchaCount; - captchaCount = this.captchas.length; - if (this.nodes.container && !this.timeouts.destroy) { - captchaCount++; - } - this.postsCount = QR.posts.length; - if (this.postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { - this.postsCount = 0; - } - return captchaCount < this.postsCount; - }, - onPostChange: function() { - if (this.postsCount === 0) { - this.setup(); - } - if (QR.posts.length === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) { - return this.postsCount = 0; - } - }, - toggle: function() { - if (this.nodes.container && !this.timeouts.destroy) { - return this.destroy(); - } else { - return this.setup(true, true); - } - }, - setup: function(focus, force) { - if (!(this.isEnabled && (this.needed() || force))) { - return; - } - $.addClass(QR.nodes.el, 'captcha-open'); - if (focus) { - this.shouldFocus = true; - } - if (this.timeouts.destroy) { - clearTimeout(this.timeouts.destroy); - delete this.timeouts.destroy; - return this.reload(); - } - if (this.nodes.container) { - return; - } - this.nodes.container = $.el('div', { - className: 'captcha-container' - }); - $.prepend(this.nodes.root, this.nodes.container); - new MutationObserver(this.afterSetup.bind(this)).observe(this.nodes.container, { - childList: true, - subtree: true - }); - return $.globalEval('(function() {\n function render() {\n var container = document.querySelector("#qr .captcha-container");\n container.dataset.widgetID = window.grecaptcha.render(container, {\n sitekey: \'6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\',\n theme: document.documentElement.classList.contains(\'tomorrow\') ? \'dark\' : \'light\',\n callback: function(response) {\n window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));\n }\n });\n }\n if (window.grecaptcha) {\n render();\n } else {\n var cbNative = window.onRecaptchaLoaded;\n window.onRecaptchaLoaded = function() {\n render();\n cbNative();\n }\n }\n})();'); - }, - afterSetup: function(mutations) { - var iframe, mutation, node, textarea, _i, _j, _len, _len1, _ref; - for (_i = 0, _len = mutations.length; _i < _len; _i++) { - mutation = mutations[_i]; - _ref = mutation.addedNodes; - for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { - node = _ref[_j]; - if (iframe = $.x('./descendant-or-self::iframe', node)) { - this.setupIFrame(iframe); - } - if (textarea = $.x('./descendant-or-self::textarea', node)) { - this.setupTextArea(textarea); - } - } - } - }, - setupIFrame: function(iframe) { - this.setupTime = Date.now(); - if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { - QR.nodes.el.style.top = null; - QR.nodes.el.style.bottom = '0px'; - } - if (this.shouldFocus) { - iframe.focus(); - } - return this.shouldFocus = false; - }, - setupTextArea: function(textarea) { - return $.one(textarea, 'input', (function(_this) { - return function() { - return _this.save(true); - }; - })(this)); - }, - destroy: function() { - if (!this.isEnabled) { - return; - } - delete this.timeouts.destroy; - $.rmClass(QR.nodes.el, 'captcha-open'); - if (this.nodes.container) { - $.rm(this.nodes.container); - } - return delete this.nodes.container; - }, - sync: function(captchas) { - if (captchas == null) { - captchas = []; - } - this.captchas = captchas; - this.clear(); - return this.count(); - }, - getOne: function() { - var captcha; - this.clear(); - if (captcha = this.captchas.shift()) { - this.count(); - $.set('captchas', this.captchas); - return captcha.response; - } else { - return null; - } - }, - save: function(pasted) { - var reload, _base; - $.forceSync('captchas'); - reload = (QR.cooldown.auto || Conf['Post on Captcha Completion']) && this.needed(); - this.captchas.push({ - response: $('textarea', this.nodes.container).value, - timeout: (pasted ? this.setupTime : Date.now()) + 2 * $.MINUTE - }); - this.count(); - $.set('captchas', this.captchas); - if (reload) { - this.shouldFocus = true; - this.reload(); - } else { - if (pasted) { - this.destroy(); - } else { - if ((_base = this.timeouts).destroy == null) { - _base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND); - } - } - QR.nodes.status.focus(); - } - if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) { - return QR.submit(); - } - }, - clear: function() { - var captcha, i, now, _i, _len, _ref; - if (!this.captchas.length) { - return; - } - $.forceSync('captchas'); - now = Date.now(); - _ref = this.captchas; - for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { - captcha = _ref[i]; - if (captcha.timeout > now) { - break; - } - } - if (!i) { - return; - } - this.captchas = this.captchas.slice(i); - this.count(); - $.set('captchas', this.captchas); - return this.setup(true); - }, - count: function() { - this.nodes.counter.textContent = "Captchas: " + this.captchas.length; - clearTimeout(this.timeouts.clear); - if (this.captchas.length) { - return this.timeouts.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); - } - }, - reload: function(focus) { - return $.globalEval('(function() {\n var container = document.querySelector("#qr .captcha-container");\n window.grecaptcha.reset(container.dataset.widgetID);\n})();'); - } - }; - QR.cooldown = { init: function() { var key, setTimers, type; @@ -18383,7 +18216,6 @@ } }, updateContext: function(view) { - g.DEAD = false; if (view === 'thread') { g.THREADID = +window.location.pathname.split('/')[3]; } diff --git a/src/General/Navigate.coffee b/src/General/Navigate.coffee index b72ee3cbd..1be69ee90 100644 --- a/src/General/Navigate.coffee +++ b/src/General/Navigate.coffee @@ -104,7 +104,6 @@ Navigate = updateContext: (view) -> # State tracking - g.DEAD = false g.THREADID = +window.location.pathname.split('/')[3] if view is 'thread' { diff --git a/src/General/html/Features/QuickReply.html b/src/General/html/Features/QuickReply.html index 06bca6f07..6bd1dad13 100755 --- a/src/General/html/Features/QuickReply.html +++ b/src/General/html/Features/QuickReply.html @@ -9,9 +9,9 @@
- + - +
diff --git a/src/Posting/QR.captcha.coffee b/src/Posting/QR.captcha.coffee deleted file mode 100644 index 2fcfa89bb..000000000 --- a/src/Posting/QR.captcha.coffee +++ /dev/null @@ -1,173 +0,0 @@ -QR.captcha = - init: -> - return if d.cookie.indexOf('pass_enabled=1') >= 0 - return unless @isEnabled = !!$.id 'g-recaptcha' - - @captchas = [] - $.get 'captchas', [], ({captchas}) -> - QR.captcha.sync captchas - $.sync 'captchas', @sync.bind @ - - root = $.el 'div', className: 'captcha-root' - $.extend root, <%= html( - '
' - ) %> - counter = $ '.captcha-counter > a', root - @nodes = {root, counter} - @count() - $.addClass QR.nodes.el, 'has-captcha' - $.after QR.nodes.com.parentNode, root - - $.on counter, 'click', @toggle.bind @ - $.on window, 'captcha:success', => - # XXX Greasemonkey 1.x workaround to gain access to GM_* functions. - $.queueTask => @save false - - shouldFocus: false - timeouts: {} - postsCount: 0 - - needed: -> - captchaCount = @captchas.length - captchaCount++ if @nodes.container and !@timeouts.destroy - @postsCount = QR.posts.length - @postsCount = 0 if @postsCount is 1 and !Conf['Auto-load captcha'] and !QR.posts[0].com and !QR.posts[0].file - captchaCount < @postsCount - - onPostChange: -> - @setup() if @postsCount is 0 - @postsCount = 0 if QR.posts.length is 1 and !Conf['Auto-load captcha'] and !QR.posts[0].com and !QR.posts[0].file - - toggle: -> - if @nodes.container and !@timeouts.destroy - @destroy() - else - @setup true, true - - setup: (focus, force) -> - return unless @isEnabled and (@needed() or force) - $.addClass QR.nodes.el, 'captcha-open' - @shouldFocus = true if focus - if @timeouts.destroy - clearTimeout @timeouts.destroy - delete @timeouts.destroy - return @reload() - - return if @nodes.container - - @nodes.container = $.el 'div', className: 'captcha-container' - $.prepend @nodes.root, @nodes.container - new MutationObserver(@afterSetup.bind @).observe @nodes.container, - childList: true - subtree: true - - $.globalEval ''' - (function() { - function render() { - var container = document.querySelector("#qr .captcha-container"); - container.dataset.widgetID = window.grecaptcha.render(container, { - sitekey: '<%= meta.recaptchaKey %>', - theme: document.documentElement.classList.contains('tomorrow') ? 'dark' : 'light', - callback: function(response) { - window.dispatchEvent(new CustomEvent("captcha:success", {detail: response})); - } - }); - } - if (window.grecaptcha) { - render(); - } else { - var cbNative = window.onRecaptchaLoaded; - window.onRecaptchaLoaded = function() { - render(); - cbNative(); - } - } - })(); - ''' - - afterSetup: (mutations) -> - for mutation in mutations - for node in mutation.addedNodes - @setupIFrame iframe if iframe = $.x './descendant-or-self::iframe', node - @setupTextArea textarea if textarea = $.x './descendant-or-self::textarea', node - return - - setupIFrame: (iframe) -> - @setupTime = Date.now() - - if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight - QR.nodes.el.style.top = null - QR.nodes.el.style.bottom = '0px' - iframe.focus() if @shouldFocus - @shouldFocus = false - - setupTextArea: (textarea) -> - $.one textarea, 'input', => @save true - - destroy: -> - return unless @isEnabled - delete @timeouts.destroy - $.rmClass QR.nodes.el, 'captcha-open' - $.rm @nodes.container if @nodes.container - delete @nodes.container - - sync: (captchas=[]) -> - @captchas = captchas - @clear() - @count() - - getOne: -> - @clear() - if captcha = @captchas.shift() - @count() - $.set 'captchas', @captchas - captcha.response - else - null - - save: (pasted) -> - $.forceSync 'captchas' - reload = (QR.cooldown.auto or Conf['Post on Captcha Completion']) and @needed() - @captchas.push - response: $('textarea', @nodes.container).value - timeout: (if pasted then @setupTime else Date.now()) + 2 * $.MINUTE - @count() - $.set 'captchas', @captchas - - if reload - @shouldFocus = true - @reload() - else - if pasted - @destroy() - else - @timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND - QR.nodes.status.focus() - - QR.submit() if Conf['Post on Captcha Completion'] and !QR.cooldown.auto - - clear: -> - return unless @captchas.length - $.forceSync 'captchas' - now = Date.now() - for captcha, i in @captchas - break if captcha.timeout > now - return unless i - @captchas = @captchas[i..] - @count() - $.set 'captchas', @captchas - @setup true - - count: -> - @nodes.counter.textContent = "Captchas: #{@captchas.length}" - clearTimeout @timeouts.clear - if @captchas.length - @timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now() - - reload: (focus) -> - $.globalEval ''' - (function() { - var container = document.querySelector("#qr .captcha-container"); - window.grecaptcha.reset(container.dataset.widgetID); - })(); - ''' diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee index 2babc361c..e4e1783b7 100644 --- a/src/Posting/QR.coffee +++ b/src/Posting/QR.coffee @@ -5,8 +5,19 @@ QR = @db = new DataBoard 'yourPosts' @posts = [] + return if g.VIEW is 'archive' + + $.globalEval 'document.documentElement.dataset.jsEnabled = true;' + noscript = Conf['Force Noscript Captcha'] or !doc.dataset.jsEnabled + @captcha = Captcha[if noscript then 'noscript' else 'v2'] + $.on d, '4chanXInitFinished', @initReady + window.addEventListener 'focus', @focus, true + window.addEventListener 'blur', @focus, true + # We don't receive blur events from captcha iframe. + $.on d, 'click', @focus + Post.callbacks.push name: 'Quick Reply' cb: @node @@ -45,9 +56,7 @@ QR = QR.postingIsEnabled = !!$.id 'postForm' return unless QR.postingIsEnabled - <% if (type === 'crx') { %> $.on d, 'paste', QR.paste - <% } %> $.on d, 'dragover', QR.dragOver $.on d, 'drop', QR.dropFile $.on d, 'dragstart dragend', QR.drag @@ -58,10 +67,12 @@ QR = return if !Conf['Persistent QR'] QR.open() - QR.hide() if Conf['Auto-Hide QR'] + QR.hide() if Conf['Auto Hide QR'] statusCheck: -> - if g.DEAD + return unless QR.nodes + {thread} = QR.posts[0] + if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead QR.abort() else QR.status() @@ -78,9 +89,9 @@ QR = open: -> if QR.nodes + QR.captcha.setup() if QR.nodes.el.hidden QR.nodes.el.hidden = false QR.unhide() - QR.captcha.setup() return try QR.dialog() @@ -107,12 +118,25 @@ QR = QR.status() QR.captcha.destroy() - focusin: -> - QR.captcha.setup() if $.hasClass(QR.nodes.el, 'autohide') and !$.hasClass(QR.nodes.el, 'focus') - $.addClass QR.nodes.el, 'focus' + focus: -> + $.queueTask -> + return unless QR.nodes + unless $$('.goog-bubble-content > iframe').some((el) -> el.getBoundingClientRect().top >= 0) + focus = d.activeElement and QR.nodes.el.contains(d.activeElement) + $[if focus then 'addClass' else 'rmClass'] QR.nodes.el, 'focus' + if chrome? + # XXX Stop anomalous scrolling on space/tab in captcha iframe. + if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME' + QR.scrollY = window.scrollY + $.on d, 'scroll', QR.scrollLock + else + $.off d, 'scroll', QR.scrollLock - focusout: -> - $.rmClass QR.nodes.el, 'focus' + scrollLock: (e) -> + if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME' + window.scroll window.scrollX, QR.scrollY + else + $.off d, 'scroll', QR.scrollLock hide: -> d.activeElement.blur() @@ -138,7 +162,9 @@ QR = el.removeAttribute 'style' if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent QR.captcha.setup true - QR.notify el + QR.captcha.notify el + else + QR.notify el alert el.textContent if d.hidden notify: (el) -> @@ -196,7 +222,7 @@ QR = sel = d.getSelection() post = Get.postFromNode @ - text = ">>#{post}\n" + text = if post.board.ID is g.BOARD.ID then ">>#{post}\n" else ">>>/#{post.board}/#{post}\n" if sel.toString().trim() and post is Get.postFromNode sel.anchorNode range = sel.getRangeAt 0 frag = range.cloneContents() @@ -356,6 +382,7 @@ QR = 'new' list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent if $.hasClass list, 'riced' + (if g.VIEW is 'thread' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread' dialog: -> QR.nodes = nodes = @@ -405,6 +432,14 @@ QR = QR.max_width_video = QR.max_height_video = 2048 QR.max_duration_video = 120 + if Conf['Show New Thread Option in Threads'] + $.addClass QR.nodes.el, 'show-new-thread-option' + + if Conf['Show Name and Subject'] + $.addClass QR.nodes.name, 'force-show' + $.addClass QR.nodes.sub, 'force-show' + QR.nodes.email.placeholder = 'E-mail' + QR.forcedAnon = !!$ 'form[name="post"] input[name="name"][type="hidden"]' if QR.forcedAnon $.addClass QR.nodes.el, 'forced-anon' @@ -420,16 +455,16 @@ QR = nodes.addPost.tabIndex = 35 if g.BOARD.ID is 'f' and g.VIEW isnt 'thread' - nodes.flashTag = $.el 'select', name: 'filetag', - innerHTML: """ - - - - - - - - """ + nodes.flashTag = $.el 'select', name: 'filetag', + $.extend nodes.flashTag, <%= html( + '' + + '' + + '' + + '' + + '' + + '' + + '' + ) %> nodes.flashTag.dataset.default = '4' $.add nodes.form, nodes.flashTag @@ -437,12 +472,6 @@ QR = $.on nodes.filename.parentNode, 'click keydown', QR.openFileInput - items = $$ '*', QR.nodes.el - i = 0 - while elm = items[i++] - $.on elm, 'blur', QR.focusout - $.on elm, 'focus', QR.focusin - $.on nodes.autohide, 'change', QR.toggleHide $.on nodes.close, 'click', QR.close $.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump' @@ -462,7 +491,7 @@ QR = $.on nodes[name], 'mouseover', QR.mouseover # save selected post's data - items = ['name', 'email', 'sub', 'com', 'filename', 'flag'] + items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag'] i = 0 save = -> QR.selected.save @ while name = items[i++] @@ -549,12 +578,13 @@ QR = $.rm nodes.flag delete nodes.flag - if g.BOARD.ID is 'pol' - flag = QR.flags() - flag.dataset.name = 'flag' - flag.dataset.default = '0' - nodes.flag = flag - $.add nodes.form, flag +# # if false? +# if g.BOARD.ID is 'pol' +# flag = QR.flags() +# flag.dataset.name = 'flag' +# flag.dataset.default = '0' +# nodes.flag = flag +# $.add nodes.form, flag submit: (e) -> e?.preventDefault() @@ -590,8 +620,8 @@ QR = err = 'Max limit of image replies has been reached.' if QR.captcha.isEnabled and !err - response = QR.captcha.getOne() - err = 'No valid captcha.' unless response + captcha = QR.captcha.getOne() + err = 'No valid captcha.' unless captcha QR.cleanNotifications() if err @@ -613,10 +643,8 @@ QR = formData = resto: threadID - name: post.name unless QR.forcedAnon email: post.email - sub: post.sub unless QR.forcedAnon or threadID com: post.com upfile: post.file @@ -626,7 +654,6 @@ QR = textonly: textOnly mode: 'regist' pwd: QR.persona.pwd - 'g-recaptcha-response': response options = responseType: 'document' @@ -639,10 +666,11 @@ QR = QR.cooldown.auto = false QR.status() QR.error $.el 'span', - innerHTML: """ - Connection error. You may have been banned. - [?] - """ + <%= html( + '4chan X encountered an error while posting. ' + + '[Banned?] ' + + '[More info]' + ) %> extra = form: $.formData formData upCallbacks: @@ -657,11 +685,29 @@ QR = QR.req.progress = "#{Math.round e.loaded / e.total * 100}%" QR.status() - QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra + cb = (response) -> + extra.form.append 'g-recaptcha-response', response if response? + QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra + QR.req.progress = '...' + + if typeof captcha is 'function' + # Wait for captcha to be verified before submitting post. + QR.req = + progress: '...' + abort: -> cb = null + captcha (response) -> + if response + cb? response + else + delete QR.req + post.unlock() + QR.cooldown.auto = !!QR.captcha.captchas.length + QR.status() + else + cb captcha + # Starting to upload might take some time. # Provide some feedback that we're starting to submit. - QR.req.uploadStartTime = Date.now() - QR.req.progress = '...' QR.status() response: -> @@ -674,17 +720,11 @@ QR = resDoc = req.response if ban = $ '.banType', resDoc # banned/warning board = $('.board', resDoc).innerHTML - err = $.el 'span', innerHTML: + err = $.el 'span', if ban.textContent.toLowerCase() is 'banned' - """ - You are banned on #{board}! ;_;
- Click here to see the reason. - """ + <%= html('You are banned on &{$(".board", resDoc)}! ;_;
Click here to see the reason.') %> else - """ - You were issued a warning on #{board} as #{$('.nameBlock', resDoc).innerHTML}.
- Reason: #{$('.reason', resDoc).innerHTML} - """ + <%= html('You were issued a warning on &{$(".board", resDoc)} as &{$(".nameBlock", resDoc)}.
Reason: &{$(".reason", resDoc)}') %> else if err = resDoc.getElementById 'errmsg' # error! $('a', err)?.target = '_blank' # duplicate image link else if resDoc.title isnt 'Post successful!' @@ -710,13 +750,14 @@ QR = false # Too many frequent mistyped captchas will auto-ban you! # On connection error, the post most likely didn't go through. - QR.cooldown.set delay: 2 - else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i + QR.cooldown.addDelay post, 2 + else if err.textContent and (m = err.textContent.match /wait\s+(\d+)\s+second/i) and !/duplicate/i.test err.textContent QR.cooldown.auto = if QR.captcha.isEnabled !!QR.captcha.captchas.length else true - QR.cooldown.set delay: m[1] + QR.cooldown.addDelay post, +m[1] + QR.captcha.setup (d.activeElement is QR.nodes.status) else # stop auto-posting QR.cooldown.auto = false QR.status() @@ -762,26 +803,27 @@ QR = notif.onclick = -> QR.open() window.focus() + QR.captcha.setup true notif.onshow = -> setTimeout -> notif.close() , 7 * $.SECOND - unless Conf['Persistent QR'] or QR.cooldown.auto + unless Conf['Persistent QR'] or postsCount QR.close() else post.rm() - QR.captcha.setup true + QR.captcha.setup(d.activeElement is QR.nodes.status) - QR.cooldown.set {req, post, isReply, threadID} + QR.cooldown.add req.uploadEndTime, threadID, postID URL = if threadID is postID # new thread - Build.path g.BOARD.ID, threadID + window.location.origin + Build.path g.BOARD.ID, threadID else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index - Build.path g.BOARD.ID, threadID, postID + window.location.origin + Build.path g.BOARD.ID, threadID, postID if URL - if Conf['Open Post in New Tab'] + if Conf['Open Post in New Tab'] or postsCount $.open URL else window.location = URL