From 2bfc3e649c5505b9488fa71184767b4282f69604 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sat, 26 Nov 2011 04:25:06 +0100 Subject: [PATCH] Put the old QR back; this is temporary. --- 4chan_x.user.js | 791 +++++++++++++++++++++++------------------------ script.coffee | 795 ++++++++++++++++++++++++------------------------ 2 files changed, 790 insertions(+), 796 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index ac4cb403f..6635bce7b 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -64,7 +64,7 @@ */ (function() { - var $, $$, DAY, Favicon, HOUR, MINUTE, Main, NAMESPACE, QR, SECOND, Time, anonymize, conf, config, d, expandComment, expandThread, filter, flatten, g, getTitle, imgExpand, imgGif, imgHover, key, keybinds, log, nav, options, quoteBacklink, quoteInline, quoteOP, quotePreview, redirect, replyHiding, reportButton, revealSpoilers, sauce, strikethroughQuotes, threadHiding, threadStats, threading, titlePost, ui, unread, updater, val, watcher; + var $, $$, DAY, Favicon, HOUR, MINUTE, Main, NAMESPACE, Recaptcha, SECOND, Time, anonymize, conf, config, cooldown, d, expandComment, expandThread, filter, flatten, g, getTitle, imgExpand, imgGif, imgHover, key, keybinds, log, nav, options, qr, quoteBacklink, quoteInline, quoteOP, quotePreview, redirect, replyHiding, reportButton, revealSpoilers, sauce, strikethroughQuotes, threadHiding, threadStats, threading, titlePost, ui, unread, updater, val, watcher; var __slice = Array.prototype.slice; config = { @@ -104,6 +104,7 @@ 'Auto Watch Reply': [false, 'Automatically watch threads that you reply to'] }, Posting: { + 'Auto Noko': [true, 'Always redirect to your post'], 'Cooldown': [true, 'Prevent `flood detected` errors'], 'Quick Reply': [true, 'Reply without leaving the page'], 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'], @@ -894,8 +895,8 @@ case conf.close: if (o = $('#overlay')) { $.rm(o); - } else if (QR.qr) { - QR.close(); + } else if (qr.qr) { + qr.close(); } break; case conf.spoiler: @@ -963,8 +964,8 @@ if ((_ref3 = $('input[value=Previous]')) != null) _ref3.click(); break; case conf.submit: - if (QR.qr) { - QR.submit.call($('form', QR.qr)); + if (qr.qr) { + qr.submit.call($('form', qr.qr)); } else { $('.postarea form').submit(); } @@ -1060,12 +1061,12 @@ }, qr: function(thread, quote) { if (quote) { - return QR.quote.call($('.quotejs + a', $('.replyhl', thread) || thread)); + return qr.quote.call($('.quotejs + a', $('.replyhl', thread) || thread)); } else { - if (QR.qr) { - return $('textarea', QR.qr).focus(); + if (qr.qr) { + return $('textarea', qr.qr).focus(); } else { - return QR.dialog('', thread != null ? thread.firstChild.id : void 0); + return qr.dialog('', thread != null ? thread.firstChild.id : void 0); } } }, @@ -1393,26 +1394,65 @@ } }; - QR = { + cooldown = { init: function() { - var holder; - if (!($('form[name=post]') && $('#recaptcha_response_field'))) return; - g.callbacks.push(function(root) { - var quote; - quote = $('.quotejs + a', root); - return $.on(quote, 'click', QR.quote); + var match, time, _; + if (match = location.search.match(/cooldown=(\d+)/)) { + _ = match[0], time = match[1]; + if ($.get(g.BOARD + '/cooldown', 0) < time) { + $.set(g.BOARD + '/cooldown', time); + } + } + if (Date.now() < $.get(g.BOARD + '/cooldown', 0)) cooldown.start(); + $.on(window, 'storage', function(e) { + if (e.key === ("" + NAMESPACE + g.BOARD + "/cooldown")) { + return cooldown.start(); + } }); - $.add(d.body, $.el('iframe', { - name: 'iframe', - hidden: true - })); - $('#recaptcha_response_field').id = ''; - holder = $('#recaptcha_challenge_field_holder'); - $.on(holder, 'DOMNodeInserted', QR.captchaNode); - QR.captchaNode({ - target: holder.firstChild - }); - QR.accept = $('.rules').textContent.match(/: (.+) /)[1].replace(/\w+/g, function(type) { + if (g.REPLY) return $('.postarea form').action += '?cooldown'; + }, + start: function() { + var submit, _i, _len, _ref; + cooldown.duration = Math.ceil(($.get(g.BOARD + '/cooldown', 0) - Date.now()) / 1000); + if (!(cooldown.duration > 0)) return; + _ref = $$('#com_submit'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + submit = _ref[_i]; + submit.value = cooldown.duration; + submit.disabled = true; + } + return setTimeout(cooldown.cb, 1000); + }, + cb: function() { + var submit, submits, _i, _j, _len, _len2, _results; + submits = $$('#com_submit'); + if (--cooldown.duration) { + setTimeout(cooldown.cb, 1000); + _results = []; + for (_i = 0, _len = submits.length; _i < _len; _i++) { + submit = submits[_i]; + _results.push(submit.value = cooldown.duration); + } + return _results; + } else { + for (_j = 0, _len2 = submits.length; _j < _len2; _j++) { + submit = submits[_j]; + submit.disabled = false; + submit.value = 'Submit'; + } + return qr.autoPost(); + } + } + }; + + qr = { + init: function() { + var iframe; + g.callbacks.push(qr.node); + $.on($('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode); + qr.captchaTime = Date.now(); + qr.spoiler = $('.postarea label') ? '' : ''; + qr.acceptFiles = $('.rules').textContent.match(/: (.+) /)[1].replace(/\w+/g, function(type) { switch (type) { case 'JPG': return 'image/JPEG'; @@ -1422,315 +1462,295 @@ return 'image/' + type; } }); - QR.MAX_FILE_SIZE = $('input[name=MAX_FILE_SIZE]').value; - QR.spoiler = $('.postarea label') ? ' ' : ''; - if (conf['Persistent QR']) { - QR.dialog(); - $('textarea', QR.qr).blur(); - if (conf['Auto Hide QR']) $('#autohide', QR.qr).checked = true; - } - if (conf['Cooldown']) { - return $.on(window, 'storage', function(e) { - if (e.key === ("" + NAMESPACE + "cooldown/" + g.BOARD)) { - return QR.cooldown(); - } - }); - } - }, - attach: function(file) { - var box, files; - files = $('#files', QR.qr); - box = $.el('li', { - innerHTML: "X" + iframe = $.el('iframe', { + name: 'iframe', + hidden: true }); - $.on($('.x', box), 'click', QR.rmThumb); - $.add(box, file); - $.add(files, box); - QR.stats(); - return QR.foo(); + $.add(d.body, iframe); + return $('#recaptcha_response_field').id = ''; }, - rmThumb: function() { - $.rm(this.parentNode); - return QR.stats(); + attach: function() { + var fileDiv; + fileDiv = $.el('div', { + innerHTML: "X" + }); + $.on(fileDiv.firstChild, 'change', qr.validateFileSize); + $.on(fileDiv.lastChild, 'click', (function() { + return $.rm(this.parentNode); + })); + return $.add($('#files', qr.el), fileDiv); + }, + attachNext: function() { + var file, fileDiv, oldFile; + fileDiv = $.rm($('#files div', qr.el)); + file = fileDiv.firstChild; + oldFile = $('#qr_form input[type=file]', qr.el); + return $.replace(oldFile, file); + }, + autoPost: function() { + if (qr.el && $('#auto', qr.el).checked) { + return qr.submit.call($('form', qr.el)); + } }, captchaNode: function(e) { - QR.captcha = { - challenge: e.target.value, - time: Date.now() - }; - return QR.captchaImg(); + if (!qr.el) return; + val = e.target.value; + $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + val; + qr.challenge = val; + return qr.captchaTime = Date.now(); }, - captchaImg: function() { - var c, qr; - qr = QR.qr; - if (!qr) return; - c = QR.captcha.challenge; - return $('#captcha img', qr).src = "http://www.google.com/recaptcha/api/image?c=" + c; - }, - captchaPush: function(el) { - var captcha, captchas; - captcha = QR.captcha; - captcha.response = el.value; + captchaKeydown: function(e) { + var captchas; + if (!(e.keyCode === 13 && this.value)) return; captchas = $.get('captchas', []); - captchas.push(captcha); + captchas.push({ + challenge: qr.challenge, + response: this.value, + time: qr.captchaTime + }); $.set('captchas', captchas); - el.value = ''; - QR.captchaReload(); - return QR.stats(captchas); + $('#captchas', qr.el).textContent = captchas.length + ' captchas'; + Recaptcha.reload(); + this.value = ''; + if (!$('textarea', qr.el).value && !$('input[type=file]', qr.el).files.length) { + return e.preventDefault(); + } }, - captchaShift: function() { - var captcha, captchas, cutoff; - captchas = $.get('captchas', []); + close: function() { + $.rm(qr.el); + return qr.el = null; + }, + dialog: function(link) { + var THREAD_ID, c, email, html, m, name, pwd, submitDisabled, submitValue; + c = d.cookie; + name = (m = c.match(/4chan_name=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; + email = (m = c.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; + pwd = (m = c.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value; + submitValue = $('#com_submit').value; + submitDisabled = $('#com_submit').disabled ? 'disabled' : ''; + THREAD_ID = g.THREAD_ID || $.x('ancestor::div[@class="thread"]/div', link).id; + qr.challenge = $('#recaptcha_challenge_field').value; + html = " X
Quick Reply
" + qr.spoiler + "
" + ($.get('captchas', []).length) + " captchas
attach another file
"; + qr.el = ui.dialog('qr', 'top: 0; left: 0;', html); + $.on($('input[name=name]', qr.el), 'mousedown', function(e) { + return e.stopPropagation(); + }); + $.on($('input[name=upfile]', qr.el), 'change', qr.validateFileSize); + $.on($('#close', qr.el), 'click', qr.close); + $.on($('form', qr.el), 'submit', qr.submit); + $.on($('#attach', qr.el), 'click', qr.attach); + $.on($('img', qr.el), 'click', Recaptcha.reload); + $.on($('#dummy', qr.el), 'keydown', Recaptcha.listener); + $.on($('#dummy', qr.el), 'keydown', qr.captchaKeydown); + return $.add(d.body, qr.el); + }, + message: function(data) { + var duration, fileCount, tc; + $('iframe[name=iframe]').src = 'about:blank'; + fileCount = $('#files', qr.el).childElementCount; + tc = data.textContent; + if (tc) { + $.extend($('#error', qr.el), data); + $('#recaptcha_response_field', qr.el).value = ''; + $('#autohide', qr.el).checked = false; + if (tc === 'You seem to have mistyped the verification.') { + setTimeout(qr.autoPost, 1000); + } else if (tc === 'Error: Duplicate file entry detected.' && fileCount) { + $('textarea', qr.el).value += '\n' + tc + ' ' + data.href; + qr.attachNext(); + setTimeout(qr.autoPost, 1000); + } + return; + } + if (qr.el) { + if (g.REPLY && (conf['Persistent QR'] || fileCount)) { + qr.refresh(); + if (fileCount) qr.attachNext(); + } else { + qr.close(); + } + } + if (conf['Cooldown']) { + duration = qr.sage ? 60 : 30; + $.set(g.BOARD + '/cooldown', Date.now() + duration * 1000); + return cooldown.start(); + } + }, + node: function(root) { + var quote; + quote = $('a.quotejs:not(:first-child)', root); + return $.on(quote, 'click', qr.quote); + }, + postInvalid: function() { + var captcha, captchas, content, cutoff, dummy, response; + content = $('textarea', qr.el).value || $('input[type=file]', qr.el).files.length; + if (!content) return 'Error: No text entered.'; + /* + captchas expire after 5 hours (emperically verified). cutoff 5 minutes + before then, b/c posting takes time. + */ cutoff = Date.now() - 5 * HOUR + 5 * MINUTE; + captchas = $.get('captchas', []); while (captcha = captchas.shift()) { if (captcha.time > cutoff) break; } $.set('captchas', captchas); - QR.stats(captchas); - return captcha; - }, - stats: function(captchas) { - var images, qr; - qr = QR.qr; - captchas || (captchas = $.get('captchas', [])); - images = $$('#files input', qr); - return $('#qr_stats', qr).textContent = "" + images.length + " / " + captchas.length; - }, - captchaReload: function() { - return window.location = 'javascript:Recaptcha.reload()'; - }, - change: function(e) { - var file, fr, img; - file = this.files[0]; - if (file.size > QR.MAX_FILE_SIZE) { - alert('Error: File too large.'); - QR.foo(this); - return; + $('#captchas', qr.el).textContent = captchas.length + ' captchas'; + if (!captcha) { + dummy = $('#dummy', qr.el); + if (!(response = dummy.value)) { + return 'You forgot to type in the verification'; + } + captcha = { + challenge: qr.challenge, + response: response + }; + dummy.value = ''; + Recaptcha.reload(); } - if (this.parentNode.className === 'wat') QR.attach(this); - fr = new FileReader(); - img = $('img', this.parentNode); - fr.onload = function(e) { - return img.src = e.target.result; - }; - return fr.readAsDataURL(file); + $('#recaptcha_challenge_field', qr.el).value = captcha.challenge; + $('#recaptcha_response_field', qr.el).value = captcha.response; + return false; }, - close: function() { - $.rm(QR.qr); - return QR.qr = null; - }, - cooldown: function() { - var b, cooldown, n, now; - if (!(g.REPLY && QR.qr)) return; - cooldown = $.get("cooldown/" + g.BOARD, 0); - now = Date.now(); - n = Math.ceil((cooldown - now) / 1000); - b = $('form button', QR.qr); - if (n > 0) { - $.extend(b, { - textContent: n, - disabled: true - }); - return setTimeout(QR.cooldown, 1000); + quote: function(e) { + var id, s, selection, selectionID, ta, text, _ref; + if (e) e.preventDefault(); + if (qr.el) { + $('#autohide', qr.el).checked = false; } else { - $.extend(b, { - textContent: 'Submit', - disabled: false - }); - if ($('#autopost', QR.qr).checked) return QR.submit(); + qr.dialog(this); } - }, - foo: function(old) { - var input; - input = $.el('input', { - type: 'file', - name: 'upfile', - accept: QR.accept - }); - $.on(input, 'change', QR.change); - if (old) { - return $.replace(old, file); - } else { - return $.add($('.wat', QR.qr), input); - } - }, - dialog: function(text, tid) { - var l, qr, ta; - if (text == null) text = ''; - tid || (tid = g.THREAD_ID || ''); - QR.qr = qr = ui.dialog('qr', 'top: 0; right: 0;', " X
" + (g.REPLY ? "" : '') + " " + QR.spoiler + "
"); - QR.reset(); - if (conf['Cooldown']) QR.cooldown(); - QR.foo(); - $.on($('.close', qr), 'click', QR.close); - $.on($('form', qr), 'submit', QR.submit); - $.on($('#recaptcha_response_field', qr), 'keydown', QR.keydown); - QR.captchaImg(); - QR.stats(); - $.add(d.body, qr); - ta = $('textarea', qr); - ta.value = text; - l = text.length; - ta.setSelectionRange(l, l); - return ta.focus(); - }, - keydown: function(e) { - var kc, v; - kc = e.keyCode; - v = this.value; - if (kc === 8 && !v) { - QR.captchaReload(); - return; - } - if (!(e.keyCode === 13 && v)) return; - QR.captchaPush(this); - e.preventDefault(); - return QR.submit(); - }, - quote: function(e, blank) { - var bq, i, id, qr, s, sel, ss, ta, text, tid, v, _base, _ref, _ref2; - if (e != null) e.preventDefault(); - tid = (_ref = $.x('ancestor::div[@class="thread"]/div', this)) != null ? _ref.id : void 0; id = this.textContent; text = ">>" + id + "\n"; - sel = getSelection(); - bq = $.x('ancestor::blockquote', sel.anchorNode); - if (id === ((_ref2 = $.x('preceding-sibling::input', bq)) != null ? _ref2.name : void 0)) { - if (s = sel.toString().replace(/\n/g, '\n>')) text += ">" + s + "\n"; - } - qr = QR.qr; - if (!qr) { - QR.dialog(text, tid); - return; - } - $('#autohide', qr).checked = false; - ta = $('textarea', qr); - v = ta.value; - ss = ta.selectionStart; - ta.value = v.slice(0, ss) + text + v.slice(ss); - i = ss + text.length; - ta.setSelectionRange(i, i); - ta.focus(); - return (_base = $('[name=resto]', qr)).value || (_base.value = tid); - }, - receive: function(data) { - var cooldown, href, qr, row, textContent, _ref, _ref2; - $('iframe[name=iframe]').src = 'about:blank'; - qr = QR.qr; - row = (_ref = $('#files input[form]', qr)) != null ? _ref.parentNode : void 0; - data = JSON.parse(data); - textContent = data.textContent, href = data.href; - if (QR.op) { - window.location = href; - return; - } - if (textContent) { - $.extend($('a.error', qr), data); - if (textContent === 'Error: Duplicate file entry detected.') { - if (row) $.rm(row); - QR.stats(); - setTimeout(QR.submit, 1000); - } else if (textContent === 'You seem to have mistyped the verification.') { - setTimeout(QR.submit, 1000); + selection = window.getSelection(); + if (s = selection.toString()) { + selectionID = (_ref = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)) != null ? _ref.name : void 0; + if (selectionID === id) { + s = s.replace(/\n/g, '\n>'); + text += ">" + s + "\n"; } - return; - } - if (row) $.rm(row); - QR.stats(); - if (conf['Persistent QR'] || ((_ref2 = $('#files input', qr)) != null ? _ref2.files.length : void 0)) { - QR.reset(); - } else { - QR.close(); - } - if (conf['Cooldown']) { - cooldown = Date.now() + (QR.sage ? 60 : 30) * SECOND; - $.set("cooldown/" + g.BOARD, cooldown); - return QR.cooldown(); } + ta = $('textarea', qr.el); + ta.focus(); + return ta.value += text; }, - reset: function() { - var c, m, qr, _ref; - qr = QR.qr; - c = d.cookie; - $('[name=name]', qr).value = (m = c.match(/4chan_name=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; - $('[name=email]', qr).value = (m = c.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; - $('[name=pwd]', qr).value = (m = c.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value; - $('[name=sub]', qr).value = ''; + refresh: function() { + var m, newFile, oldFile, _ref; + $('[name=sub]', qr.el).value = ''; + $('[name=email]', qr.el).value = (m = d.cookie.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; + $('[name=com]', qr.el).value = ''; + $('[name=recaptcha_response_field]', qr.el).value = ''; if (!conf['Remember Spoiler']) { - if ((_ref = $('[name=spoiler]', qr)) != null) _ref.checked = false; + if ((_ref = $('[name=spoiler]', qr.el)) != null) _ref.checked = false; } - return $('textarea', qr).value = ''; + oldFile = $('[type=file]', qr.el); + newFile = $.el('input', { + type: 'file', + name: 'upfile', + accept: qr.acceptFiles + }); + return $.replace(oldFile, newFile); }, submit: function(e) { - var captcha, challenge, el, id, input, op, qr, response; - qr = QR.qr; - if ($('textarea', qr).value || $('#files', qr).childNodes.length) { - if ($('form button', qr).disabled) { - $('#autopost', qr).checked = true; - return; - } - } else { - if (e) { - alert('Error: No text entered.'); - e.preventDefault(); + var id, msg, op; + if (msg = qr.postInvalid()) { + if (typeof e.preventDefault === "function") e.preventDefault(); + alert(msg); + if (msg === 'You forgot to type in the verification.') { + $('#dummy', qr.el).focus(); } return; } - $('.error', qr).textContent = ''; - if (e && (el = $('#recaptcha_response_field', qr)).value) QR.captchaPush(el); - if (!(captcha = QR.captchaShift())) { - alert('You forgot to type in the verification.'); - if (e != null) e.preventDefault(); - return; - } - challenge = captcha.challenge, response = captcha.response; - $('#challenge', qr).value = challenge; - $('#response', qr).value = response; - if (conf['Auto Hide QR']) $('#autohide', qr).checked = true; - if (input = $('#files input', qr)) input.setAttribute('form', 'qr_form'); - if (!e) $('#qr_form', qr).submit(); - QR.sage = /sage/i.test($('[name=email]', qr).value); - id = $('input[name=resto]', qr).value; - QR.op = !id; - if (QR.op) $('[name=email]', qr).value = 'noko'; - if (conf['Thread Watcher'] && conf['Auto Watch Reply']) { - op = $.id(id); - if ($('img.favicon', op).src === Favicon.empty) { - return watcher.watch(op, id); + if (conf['Auto Watch Reply'] && conf['Thread Watcher']) { + if (g.REPLY && $('img.favicon').src === Favicon.empty) { + watcher.watch(null, g.THREAD_ID); + } else { + id = $('input[name=resto]', qr.el).value; + op = $.id(id); + if ($('img.favicon', op).src === Favicon.empty) watcher.watch(op, id); } } + if (!e) this.submit(); + $('#error', qr.el).textContent = ''; + if (conf['Auto Hide QR']) $('#autohide', qr.el).checked = true; + return qr.sage = /sage/i.test($('input[name=email]', this).value); }, sys: function() { - var recaptcha; - $.off(d, 'DOMContentLoaded', QR.sys); + var c, duration, id, noko, recaptcha, sage, search, thread, url, watch, _, _ref, _ref2; if (recaptcha = $('#recaptcha_response_field')) { - $.on(recaptcha, 'keydown', QR.keydown); + $.on(recaptcha, 'keydown', Recaptcha.listener); return; } /* - http://code.google.com/p/chromium/issues/detail?id=20773 - Let content scripts see other frames (instead of them being undefined) + http://code.google.com/p/chromium/issues/detail?id=20773 + Let content scripts see other frames (instead of them being undefined) - To access the parent, we have to break out of the sandbox and evaluate - in the global context. + To access the parent, we have to break out of the sandbox and evaluate + in the global context. */ - return $.globalEval(function() { - var data, href, node, textContent, _ref; - $ = function(css) { - return document.querySelector(css); + $.globalEval(function() { + var data, node, _ref; + data = { + to: 'qr.message' }; - if (node = (_ref = $('table font b')) != null ? _ref.firstChild : void 0) { - textContent = node.textContent, href = node.href; - } else { - node = $('meta'); - href = node.content.match(/url=(.+)/)[1]; + if (node = (_ref = document.querySelector('table font b')) != null ? _ref.firstChild : void 0) { + data.textContent = node.textContent; + data.href = node.href; } - data = JSON.stringify({ - textContent: textContent, - href: href - }); return parent.postMessage(data, '*'); }); + c = (_ref = $('b')) != null ? _ref.lastChild : void 0; + if (!(c && c.nodeType === 8)) return; + _ref2 = c.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref2[0], thread = _ref2[1], id = _ref2[2]; + search = location.search; + cooldown = /cooldown/.test(search); + noko = /noko/.test(search); + sage = /sage/.test(search); + watch = /watch/.test(search); + url = "http://boards.4chan.org/" + g.BOARD; + if (watch && thread === '0') { + url += "/res/" + id + "?watch"; + } else if (noko) { + url += '/res/'; + url += thread === '0' ? id : thread; + } + if (cooldown) { + duration = Date.now() + (sage ? 60 : 30) * 1000; + url += '?cooldown=' + duration; + } + if (noko) url += '#' + id; + return window.location = url; + }, + validateFileSize: function(e) { + var file; + if (!(this.files[0].size > $('input[name=MAX_FILE_SIZE]').value)) return; + file = $.el('input', { + type: 'file', + name: 'upfile', + accept: qr.acceptFiles + }); + $.on(file, 'change', qr.validateFileSize); + $.replace(this, file); + $('#error', qr.el).textContent = 'Error: File too large.'; + return alert('Error: File too large.'); + } + }; + + Recaptcha = { + init: function() { + var el, _i, _len, _ref; + _ref = $$('#recaptcha_table a'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + el = _ref[_i]; + el.tabIndex = 1; + } + return $.bind($('#recaptcha_response_field'), 'keydown', Recaptcha.listener); + }, + listener: function(e) { + if (e.keyCode === 8 && this.value === '') return Recaptcha.reload(); + }, + reload: function() { + return window.location = 'javascript:Recaptcha.reload()'; } }; @@ -2872,15 +2892,6 @@ Main = { init: function() { var cutoff, hiddenThreads, id, lastChecked, now, pathname, temp, timestamp, _ref; - if (location.hostname === 'sys.4chan.org') { - if (d.body) { - QR.sys(); - } else { - $.on(d, 'DOMContentLoaded', QR.sys); - } - return; - } - $.on(window, 'message', Main.message); pathname = location.pathname.substring(1).split('/'); g.BOARD = pathname[0], temp = pathname[1]; if (temp === 'res') { @@ -2889,6 +2900,15 @@ } else { g.PAGENUM = parseInt(temp) || 0; } + if (location.hostname === 'sys.4chan.org') { + if (d.body) { + qr.sys(); + } else { + $.on(d, 'DOMContentLoaded', qr.sys); + } + return; + } + $.on(window, 'message', Main.message); g.hiddenReplies = $.get("hiddenReplies/" + g.BOARD + "/", {}); lastChecked = $.get('lastChecked', 0); now = Date.now(); @@ -2929,7 +2949,7 @@ } }, onLoad: function() { - var callback, node, nodes, _i, _j, _len, _len2, _ref; + var callback, canPost, form, node, nodes, _i, _j, _len, _len2, _ref; $.off(d, 'DOMContentLoaded', Main.onLoad); if (conf['404 Redirect'] && d.title === '4chan - 404' && /^\d+$/.test(g.THREAD_ID)) { redirect(); @@ -2939,9 +2959,21 @@ $.addStyle(Main.css); threading.init(); Favicon.init(); + if ((form = $('form[name=post]')) && (canPost = !!$('#recaptcha_response_field'))) { + Recaptcha.init(); + if (g.REPLY && conf['Auto Watch Reply'] && conf['Thread Watcher']) { + $.bind(form, 'submit', function() { + if ($('img.favicon').src === Favicon.empty) { + return watcher.watch(null, g.THREAD_ID); + } + }); + } + } + if (conf['Auto Noko'] && canPost) form.action += '?noko'; + if (conf['Cooldown'] && canPost) cooldown.init(); if (conf['Image Expansion']) imgExpand.init(); if (conf['Reveal Spoilers'] && $('.postarea label')) revealSpoilers.init(); - if (conf['Quick Reply']) QR.init(); + if (conf['Quick Reply']) qr.init(); if (conf['Thread Watcher']) watcher.init(); if (conf['Keybinds']) keybinds.init(); if (g.REPLY) { @@ -2950,6 +2982,10 @@ if (conf['Reply Navigation']) nav.init(); if (conf['Post in Title']) titlePost.init(); if (conf['Unread Count']) unread.init(); + if (conf['Quick Reply'] && conf['Persistent QR'] && canPost) { + qr.dialog(); + if (conf['Auto Hide QR']) $('#autohide', qr.el).checked = true; + } } else { if (conf['Thread Hiding']) threadHiding.init(); if (conf['Thread Expansion']) expandThread.init(); @@ -2975,7 +3011,7 @@ message: function(e) { var data, origin; origin = e.origin, data = e.data; - if (origin === 'http://sys.4chan.org') return QR.receive(data); + if (origin === 'http://sys.4chan.org') return qr.message(data); }, node: function(e) { var callback, target, _i, _len, _ref, _results; @@ -3001,7 +3037,7 @@ div.dialog > div.move {\ cursor: move;\ }\ - label, a, .favicon {\ + label, a, .favicon, #qr img {\ cursor: pointer;\ }\ \ @@ -3011,6 +3047,12 @@ .error {\ color: red;\ }\ + #error {\ + cursor: default;\ + }\ + #error[href] {\ + cursor: pointer;\ + }\ td.replyhider {\ vertical-align: top;\ }\ @@ -3091,6 +3133,47 @@ resize: vertical;\ width: 100%;\ }\ +\ + #qr {\ + position: fixed;\ + max-height: 100%;\ + overflow-x: hidden;\ + overflow-y: auto;\ + }\ + #qr > div.move {\ + text-align: right;\ + }\ + #qr input[name=name] {\ + float: left;\ + }\ + #qr_form {\ + clear: left;\ + }\ + #qr_form, #qr #com_submit, #qr input[name=upfile] {\ + margin: 0;\ + }\ + #qr textarea {\ + width: 100%;\ + height: 125px;\ + }\ + #qr #close, #qr #autohide {\ + float: right;\ + }\ + #qr:not(:hover) > #autohide:checked ~ .autohide {\ + height: 0;\ + overflow: hidden;\ + }\ + /* http://stackoverflow.com/questions/2610497/change-an-inputs-html5-placeholder-color-with-css */\ + #qr input::-webkit-input-placeholder {\ + color: grey;\ + }\ + #qr input:-moz-placeholder {\ + color: grey;\ + }\ + /* qr reCAPTCHA */\ + #qr img {\ + border: 1px solid #AAA;\ + }\ \ #updater {\ position: fixed;\ @@ -3163,102 +3246,6 @@ #files > input {\ display: block;\ }\ - #qr {\ - position: fixed;\ - }\ - #qr .close, #qr #autohide {\ - float: right;\ - }\ - #qr > .move {\ - text-align: right;\ - }\ - #qr .autohide > input {\ - width: 90px;\ - }\ - #qr #autopost {\ - width: auto;\ - }\ - #qr #recaptcha_response_field {\ - width: 100%;\ - }\ - #qr form {\ - margin: 0;\ - }\ - #qr .autohide {\ - clear: both;\ - }\ - #qr:not(:hover) #autohide:checked ~ .autohide {\ - height: 0;\ - overflow: hidden;\ - }\ - #qr textarea {\ - border: 0;\ - height: 150px;\ - width: 100%;\ - }\ - #qr #captcha {\ - position: relative;\ - }\ - #qr #files {\ - width: 300px;\ - white-space: nowrap;\ - overflow: auto;\ - margin: 0;\ - padding: 0;\ - }\ - #qr #files li {\ - position: relative;\ - display: inline-block;\ - width: 100px;\ - height: 100px;\ - overflow: hidden;\ - }\ - #qr #files a {\ - position: absolute;\ - left: 0;\ - font-size: 50px;\ - color: red;\ - z-index: 1;\ - }\ - #qr #cl {\ - right: 0;\ - padding: 2px;\ - position: absolute;\ - }\ - #qr #files input {\ - /* cannot use `display: none;`\ - https://bugs.webkit.org/show_bug.cgi?id=58208\ - http://code.google.com/p/chromium/issues/detail?id=78961\ - */\ - font-size: 100px;\ - opacity: 0;\ - }\ - #qr #files img {\ - position: absolute;\ - left: 0;\ - max-height: 100px;\ - max-width: 100px;\ - }\ - #qr input[name=resto] {\ - width: 80px;\ - }\ - #qr button + input[type=file] {\ - position: absolute;\ - opacity: 0;\ - pointer-events: none;\ - }\ - #qr .wat {\ - display: inline-block;\ - width: 16px;\ - overflow: hidden;\ - position: relative;\ - vertical-align: text-top;\ - }\ - #qr .wat input {\ - opacity: 0;\ - position: absolute;\ - left: 0;\ - }\ ' }; diff --git a/script.coffee b/script.coffee index 6be12598b..fbd6a345c 100644 --- a/script.coffee +++ b/script.coffee @@ -31,6 +31,7 @@ config = 'Auto Watch': [true, 'Automatically watch threads that you start'] 'Auto Watch Reply': [false, 'Automatically watch threads that you reply to'] Posting: + 'Auto Noko': [true, 'Always redirect to your post'] 'Cooldown': [true, 'Prevent `flood detected` errors'] 'Quick Reply': [true, 'Reply without leaving the page'] 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'] @@ -642,8 +643,8 @@ keybinds = when conf.close if o = $ '#overlay' $.rm o - else if QR.qr - QR.close() + else if qr.qr + qr.close() when conf.spoiler ta = e.target return unless ta.nodeName is 'TEXTAREA' @@ -694,8 +695,8 @@ keybinds = when conf.previousPage $('input[value=Previous]')?.click() when conf.submit - if QR.qr - QR.submit.call $ 'form', QR.qr + if qr.qr + qr.submit.call $ 'form', qr.qr else $('.postarea form').submit() when conf.unreadCountTo0 @@ -740,12 +741,12 @@ keybinds = qr: (thread, quote) -> if quote - QR.quote.call $ '.quotejs + a', $('.replyhl', thread) or thread + qr.quote.call $ '.quotejs + a', $('.replyhl', thread) or thread else - if QR.qr - $('textarea', QR.qr).focus() + if qr.qr + $('textarea', qr.qr).focus() else - QR.dialog '', thread?.firstChild.id + qr.dialog '', thread?.firstChild.id open: (thread, tab) -> id = thread.firstChild.id @@ -1033,25 +1034,49 @@ options = conf['backlink'] = @value $('#backlinkPreview').textContent = conf['backlink'].replace /%id/, '123456789' -QR = - #captcha caching for report form - #report queueing - #check if captchas can be reused on eg dup file error +cooldown = + #TODO merge into qr init: -> - #can't reply in some stickies, recaptcha may be blocked, eg by noscript - return unless $('form[name=post]') and $('#recaptcha_response_field') - g.callbacks.push (root) -> - quote = $ '.quotejs + a', root - $.on quote, 'click', QR.quote - $.add d.body, $.el 'iframe', - name: 'iframe' - hidden: true - # nuke id so qr's field focuses on recaptcha reload, instead of normal form's - $('#recaptcha_response_field').id = '' - holder = $ '#recaptcha_challenge_field_holder' - $.on holder, 'DOMNodeInserted', QR.captchaNode - QR.captchaNode target: holder.firstChild - QR.accept = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) -> + if match = location.search.match /cooldown=(\d+)/ + [_, time] = match + $.set g.BOARD+'/cooldown', time if $.get(g.BOARD+'/cooldown', 0) < time + cooldown.start() if Date.now() < $.get g.BOARD+'/cooldown', 0 + $.on window, 'storage', (e) -> cooldown.start() if e.key is "#{NAMESPACE}#{g.BOARD}/cooldown" + $('.postarea form').action += '?cooldown' if g.REPLY + + start: -> + cooldown.duration = Math.ceil ($.get(g.BOARD+'/cooldown', 0) - Date.now()) / 1000 + return unless cooldown.duration > 0 + for submit in $$ '#com_submit' + submit.value = cooldown.duration + submit.disabled = true + setTimeout cooldown.cb, 1000 + + cb: -> + submits = $$ '#com_submit' + if --cooldown.duration + setTimeout cooldown.cb, 1000 + for submit in submits + submit.value = cooldown.duration + else + for submit in submits + submit.disabled = false + submit.value = 'Submit' + qr.autoPost() + +qr = + # TODO + # error handling / logging + # persistent captcha + # rm Recaptcha + # email reverts + init: -> + g.callbacks.push qr.node + $.on $('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode + qr.captchaTime = Date.now() + + qr.spoiler = if $('.postarea label') then '' else '' + qr.acceptFiles = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) -> switch type when 'JPG' 'image/JPEG' @@ -1059,281 +1084,295 @@ QR = 'application/' + type else 'image/' + type - QR.MAX_FILE_SIZE = $('input[name=MAX_FILE_SIZE]').value - QR.spoiler = if $('.postarea label') then ' ' else '' - if conf['Persistent QR'] - QR.dialog() - $('textarea', QR.qr).blur() - if conf['Auto Hide QR'] - $('#autohide', QR.qr).checked = true - if conf['Cooldown'] - $.on window, 'storage', (e) -> QR.cooldown() if e.key is "#{NAMESPACE}cooldown/#{g.BOARD}" - attach: (file) -> - files = $ '#files', QR.qr - box = $.el 'li', - innerHTML: "X" - $.on $('.x', box), 'click', QR.rmThumb - $.add box, file - $.add files, box - QR.stats() - QR.foo() - rmThumb: -> - $.rm @parentNode - QR.stats() + + iframe = $.el 'iframe', + name: 'iframe' + hidden: true + $.add d.body, iframe + + #hack - nuke id so it doesn't grab focus when reloading + $('#recaptcha_response_field').id = '' + + attach: -> + fileDiv = $.el 'div', innerHTML: "X" + $.on fileDiv.firstChild, 'change', qr.validateFileSize + $.on fileDiv.lastChild, 'click', (-> $.rm @parentNode) + $.add $('#files', qr.el), fileDiv + + attachNext: -> + fileDiv = $.rm $('#files div', qr.el) + file = fileDiv.firstChild + oldFile = $ '#qr_form input[type=file]', qr.el + $.replace oldFile, file + + autoPost: -> + if qr.el and $('#auto', qr.el).checked + qr.submit.call $ 'form', qr.el + captchaNode: (e) -> - QR.captcha = - challenge: e.target.value - time: Date.now() - QR.captchaImg() - captchaImg: -> - {qr} = QR - return unless qr - c = QR.captcha.challenge - $('#captcha img', qr).src = "http://www.google.com/recaptcha/api/image?c=#{c}" - captchaPush: (el) -> - {captcha} = QR - captcha.response = el.value + return unless qr.el + val = e.target.value + $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + val + qr.challenge = val + qr.captchaTime = Date.now() + + captchaKeydown: (e) -> + return unless e.keyCode is 13 and @value #enter, captcha filled + captchas = $.get 'captchas', [] - captchas.push captcha + captchas.push + challenge: qr.challenge + response: @value + time: qr.captchaTime $.set 'captchas', captchas - el.value = '' - QR.captchaReload() - QR.stats captchas - captchaShift: -> - captchas = $.get 'captchas', [] + $('#captchas', qr.el).textContent = captchas.length + ' captchas' + Recaptcha.reload() + @value = '' + + if !$('textarea', qr.el).value and !$('input[type=file]', qr.el).files.length + e.preventDefault() + + close: -> + $.rm qr.el + qr.el = null + + dialog: (link) -> + c = d.cookie + name = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else '' + email = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else '' + pwd = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value + submitValue = $('#com_submit').value + submitDisabled = if $('#com_submit').disabled then 'disabled' else '' + #FIXME inlined cross-thread quotes + THREAD_ID = g.THREAD_ID or $.x('ancestor::div[@class="thread"]/div', link).id + qr.challenge = $('#recaptcha_challenge_field').value + + html = " + X + +
+ + Quick Reply +
+
+
+ + + + +
#{qr.spoiler}
+
+
+
+
#{$.get('captchas', []).length} captchas
+
+
+
+
attach another file
+
+ + " + qr.el = ui.dialog 'qr', 'top: 0; left: 0;', html + + $.on $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation() + $.on $('input[name=upfile]', qr.el), 'change', qr.validateFileSize + $.on $('#close', qr.el), 'click', qr.close + $.on $('form', qr.el), 'submit', qr.submit + $.on $('#attach', qr.el), 'click', qr.attach + $.on $('img', qr.el), 'click', Recaptcha.reload + $.on $('#dummy', qr.el), 'keydown', Recaptcha.listener + $.on $('#dummy', qr.el), 'keydown', qr.captchaKeydown + + $.add d.body, qr.el + + message: (data) -> + $('iframe[name=iframe]').src = 'about:blank' + fileCount = $('#files', qr.el).childElementCount + + tc = data.textContent + if tc # error message + $.extend $('#error', qr.el), data + $('#recaptcha_response_field', qr.el).value = '' + $('#autohide', qr.el).checked = false + if tc is 'You seem to have mistyped the verification.' + setTimeout qr.autoPost, 1000 + else if tc is 'Error: Duplicate file entry detected.' and fileCount + $('textarea', qr.el).value += '\n' + tc + ' ' + data.href + qr.attachNext() + setTimeout qr.autoPost, 1000 + return + + if qr.el + if g.REPLY and (conf['Persistent QR'] or fileCount) + qr.refresh() + if fileCount + qr.attachNext() + else + qr.close() + if conf['Cooldown'] + duration = if qr.sage then 60 else 30 + $.set g.BOARD+'/cooldown', Date.now() + duration * 1000 + cooldown.start() + + node: (root) -> + quote = $ 'a.quotejs:not(:first-child)', root + $.on quote, 'click', qr.quote + + postInvalid: -> + content = $('textarea', qr.el).value or $('input[type=file]', qr.el).files.length + return 'Error: No text entered.' unless content + + ### + captchas expire after 5 hours (emperically verified). cutoff 5 minutes + before then, b/c posting takes time. + ### + cutoff = Date.now() - 5*HOUR + 5*MINUTE + captchas = $.get 'captchas', [] while captcha = captchas.shift() if captcha.time > cutoff break $.set 'captchas', captchas - QR.stats captchas - captcha - stats: (captchas) -> - {qr} = QR - captchas or= $.get 'captchas', [] - images = $$ '#files input', qr - $('#qr_stats', qr).textContent = "#{images.length} / #{captchas.length}" - captchaReload: -> - window.location = 'javascript:Recaptcha.reload()' - change: (e) -> - file = @files[0] - if file.size > QR.MAX_FILE_SIZE - alert 'Error: File too large.' - QR.foo @ - return - if @parentNode.className is 'wat' - QR.attach @ - fr = new FileReader() - img = $ 'img', @parentNode - fr.onload = (e) -> - img.src = e.target.result - fr.readAsDataURL file - close: -> - $.rm QR.qr - QR.qr = null - cooldown: -> - return unless g.REPLY and QR.qr - cooldown = $.get "cooldown/#{g.BOARD}", 0 - now = Date.now() - n = Math.ceil (cooldown - now) / 1000 - b = $ 'form button', QR.qr - if n > 0 - $.extend b, - textContent: n - disabled: true - setTimeout QR.cooldown, 1000 + + $('#captchas', qr.el).textContent = captchas.length + ' captchas' + + unless captcha + dummy = $ '#dummy', qr.el + return 'You forgot to type in the verification' unless response = dummy.value + captcha = + challenge: qr.challenge + response: response + dummy.value = '' + Recaptcha.reload() + + $('#recaptcha_challenge_field', qr.el).value = captcha.challenge + $('#recaptcha_response_field', qr.el).value = captcha.response + + false + + quote: (e) -> + e.preventDefault() if e + + if qr.el + $('#autohide', qr.el).checked = false else - $.extend b, - textContent: 'Submit' - disabled: false - QR.submit() if $('#autopost', QR.qr).checked - foo: (old) -> - input = $.el 'input', - type: 'file' - name: 'upfile' - accept: QR.accept - $.on input, 'change', QR.change - if old - $.replace old, file - else - $.add $('.wat', QR.qr), input - dialog: (text='', tid) -> - tid or= g.THREAD_ID or '' - QR.qr = qr = ui.dialog 'qr', 'top: 0; right: 0;', " - X - -
- -
-
- - - - - -
- - -
-
- -
-
- - #{if g.REPLY then "" else ''} - - #{QR.spoiler} -
-
-
- - " - #XXX use dom methods to set values instead of injecting raw user input into your html -_-; - QR.reset() - QR.cooldown() if conf['Cooldown'] - QR.foo() - $.on $('.close', qr), 'click', QR.close - $.on $('form', qr), 'submit', QR.submit - $.on $('#recaptcha_response_field', qr), 'keydown', QR.keydown - QR.captchaImg() - QR.stats() - $.add d.body, qr - ta = $ 'textarea', qr - ta.value = text - l = text.length - ta.setSelectionRange l, l - ta.focus() - keydown: (e) -> - kc = e.keyCode - v = @value - if kc is 8 and not v #backspace, empty - QR.captchaReload() - return - return unless e.keyCode is 13 and v #enter, not empty - QR.captchaPush @ - e.preventDefault() - QR.submit() #derpy, but prevents checking for content twice - quote: (e, blank) -> - e?.preventDefault() - tid = $.x('ancestor::div[@class="thread"]/div', @)?.id + qr.dialog @ + id = @textContent text = ">>#{id}\n" - sel = getSelection() - bq = $.x('ancestor::blockquote', sel.anchorNode) - if id == $.x('preceding-sibling::input', bq)?.name - if s = sel.toString().replace /\n/g, '\n>' - text += ">#{s}\n" - {qr} = QR - if not qr - QR.dialog text, tid - return - $('#autohide', qr).checked = false - ta = $ 'textarea', qr - v = ta.value - ss = ta.selectionStart - ta.value = v[0...ss] + text + v[ss..] - i = ss + text.length - ta.setSelectionRange i, i - ta.focus() - $('[name=resto]', qr).value or= tid - receive: (data) -> - $('iframe[name=iframe]').src = 'about:blank' - {qr} = QR - row = $('#files input[form]', qr)?.parentNode - data = JSON.parse data - {textContent, href} = data - if QR.op - window.location = href - return - if textContent - $.extend $('a.error', qr), data - if textContent is 'Error: Duplicate file entry detected.' - $.rm row if row - QR.stats() - setTimeout QR.submit, 1000 - else if textContent is 'You seem to have mistyped the verification.' - setTimeout QR.submit, 1000 - return - $.rm row if row - QR.stats() - if conf['Persistent QR'] or $('#files input', qr)?.files.length - QR.reset() - else - QR.close() - if conf['Cooldown'] - cooldown = Date.now() + (if QR.sage then 60 else 30)*SECOND - $.set "cooldown/#{g.BOARD}", cooldown - QR.cooldown() - reset: -> - {qr} = QR - c = d.cookie - $('[name=name]', qr).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else '' - $('[name=email]', qr).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else '' - $('[name=pwd]', qr).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value - $('[name=sub]', qr).value = '' - $('[name=spoiler]', qr)?.checked = false unless conf['Remember Spoiler'] - $('textarea', qr).value = '' - submit: (e) -> - {qr} = QR - #XXX e is undefined if method is called explicitly, eg, from auto posting - if $('textarea', qr).value or $('#files', qr).childNodes.length - if $('form button', qr).disabled - $('#autopost', qr).checked = true - return - else - if e - alert 'Error: No text entered.' - e.preventDefault() - return - $('.error', qr).textContent = '' - if e and (el = $('#recaptcha_response_field', qr)).value - QR.captchaPush el - if not captcha = QR.captchaShift() - alert 'You forgot to type in the verification.' - e?.preventDefault() - return - {challenge, response} = captcha - $('#challenge', qr).value = challenge - $('#response', qr).value = response - $('#autohide', qr).checked = true if conf['Auto Hide QR'] - if input = $ '#files input', qr - input.setAttribute 'form', 'qr_form' - $('#qr_form', qr).submit() if not e - QR.sage = /sage/i.test $('[name=email]', qr).value - id = $('input[name=resto]', qr).value - QR.op = not id - $('[name=email]', qr).value = 'noko' if QR.op - if conf['Thread Watcher'] and conf['Auto Watch Reply'] - op = $.id id - if $('img.favicon', op).src is Favicon.empty - watcher.watch op, id - sys: -> - $.off d, 'DOMContentLoaded', QR.sys - if recaptcha = $ '#recaptcha_response_field' #post reporting - $.on recaptcha, 'keydown', QR.keydown - return - ### - http://code.google.com/p/chromium/issues/detail?id=20773 - Let content scripts see other frames (instead of them being undefined) - To access the parent, we have to break out of the sandbox and evaluate - in the global context. + selection = window.getSelection() + if s = selection.toString() + selectionID = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)?.name + if selectionID == id + s = s.replace /\n/g, '\n>' + text += ">#{s}\n" + + ta = $ 'textarea', qr.el + ta.focus() + ta.value += text + + refresh: -> + $('[name=sub]', qr.el).value = '' + $('[name=email]', qr.el).value = if m = d.cookie.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else '' + $('[name=com]', qr.el).value = '' + $('[name=recaptcha_response_field]', qr.el).value = '' + $('[name=spoiler]', qr.el)?.checked = false unless conf['Remember Spoiler'] + # XXX opera doesn't allow resetting file inputs w/ file.value = '' + oldFile = $ '[type=file]', qr.el + newFile = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles + $.replace oldFile, newFile + + submit: (e) -> + #XXX `e` won't exist if we're here from `qr.submit.call form`. + if msg = qr.postInvalid() + e.preventDefault?() + alert msg + if msg is 'You forgot to type in the verification.' + $('#dummy', qr.el).focus() + return + + if conf['Auto Watch Reply'] and conf['Thread Watcher'] + if g.REPLY and $('img.favicon').src is Favicon.empty + watcher.watch null, g.THREAD_ID + else + id = $('input[name=resto]', qr.el).value + op = $.id id + if $('img.favicon', op).src is Favicon.empty + watcher.watch op, id + + if !e then @submit() + $('#error', qr.el).textContent = '' + $('#autohide', qr.el).checked = true if conf['Auto Hide QR'] + qr.sage = /sage/i.test $('input[name=email]', @).value + + sys: -> + if recaptcha = $ '#recaptcha_response_field' #post reporting + $.on recaptcha, 'keydown', Recaptcha.listener + return + + ### + http://code.google.com/p/chromium/issues/detail?id=20773 + Let content scripts see other frames (instead of them being undefined) + + To access the parent, we have to break out of the sandbox and evaluate + in the global context. ### $.globalEval -> - $ = (css) -> document.querySelector css - if node = $('table font b')?.firstChild - {textContent, href} = node - else - node = $ 'meta' - href = node.content.match(/url=(.+)/)[1] - data = JSON.stringify { textContent, href } + data = to: 'qr.message' + if node = document.querySelector('table font b')?.firstChild + data.textContent = node.textContent + data.href = node.href parent.postMessage data, '*' - #if we're an iframe, parent will blank us + + c = $('b')?.lastChild + + return unless c and c.nodeType is 8 #comment node + + [_, thread, id] = c.textContent.match(/thread:(\d+),no:(\d+)/) + + {search} = location + cooldown = /cooldown/.test search + noko = /noko/ .test search + sage = /sage/ .test search + watch = /watch/ .test search + + url = "http://boards.4chan.org/#{g.BOARD}" + + if watch and thread is '0' + url += "/res/#{id}?watch" + else if noko + url += '/res/' + url += if thread is '0' then id else thread + if cooldown + duration = Date.now() + (if sage then 60 else 30) * 1000 + url += '?cooldown=' + duration + if noko + url += '#' + id + + window.location = url + + validateFileSize: (e) -> + return unless @files[0].size > $('input[name=MAX_FILE_SIZE]').value + + file = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles + $.on file, 'change', qr.validateFileSize + $.replace @, file + + $('#error', qr.el).textContent = 'Error: File too large.' + alert 'Error: File too large.' + +Recaptcha = + init: -> + #hack to tab from comment straight to recaptcha + for el in $$ '#recaptcha_table a' + el.tabIndex = 1 + $.bind $('#recaptcha_response_field'), 'keydown', Recaptcha.listener + listener: (e) -> + if e.keyCode is 8 and @value is '' # backspace to reload + Recaptcha.reload() + reload: -> + window.location = 'javascript:Recaptcha.reload()' threading = init: -> @@ -2179,15 +2218,6 @@ imgExpand = Main = init: -> - if location.hostname is 'sys.4chan.org' - if d.body - QR.sys() - else - $.on d, 'DOMContentLoaded', QR.sys - return - - $.on window, 'message', Main.message - pathname = location.pathname.substring(1).split('/') [g.BOARD, temp] = pathname if temp is 'res' @@ -2196,6 +2226,15 @@ Main = else g.PAGENUM = parseInt(temp) or 0 + if location.hostname is 'sys.4chan.org' + if d.body + qr.sys() + else + $.on d, 'DOMContentLoaded', qr.sys + return + + $.on window, 'message', Main.message + g.hiddenReplies = $.get "hiddenReplies/#{g.BOARD}/", {} lastChecked = $.get 'lastChecked', 0 @@ -2277,8 +2316,20 @@ Main = threading.init() Favicon.init() + #recaptcha may be blocked, eg by noscript + if (form = $ 'form[name=post]') and (canPost = !!$ '#recaptcha_response_field') + Recaptcha.init() + if g.REPLY and conf['Auto Watch Reply'] and conf['Thread Watcher'] + $.bind form, 'submit', -> if $('img.favicon').src is Favicon.empty + watcher.watch null, g.THREAD_ID #major features + if conf['Auto Noko'] and canPost + form.action += '?noko' + + if conf['Cooldown'] and canPost + cooldown.init() + if conf['Image Expansion'] imgExpand.init() @@ -2286,7 +2337,7 @@ Main = revealSpoilers.init() if conf['Quick Reply'] - QR.init() + qr.init() if conf['Thread Watcher'] watcher.init() @@ -2310,6 +2361,11 @@ Main = if conf['Unread Count'] unread.init() + if conf['Quick Reply'] and conf['Persistent QR'] and canPost + qr.dialog() + if conf['Auto Hide QR'] + $('#autohide', qr.el).checked = true + else #not reply if conf['Thread Hiding'] threadHiding.init() @@ -2337,7 +2393,7 @@ Main = message: (e) -> {origin, data} = e if origin is 'http://sys.4chan.org' - QR.receive data + qr.message data node: (e) -> {target} = e @@ -2356,7 +2412,7 @@ Main = div.dialog > div.move { cursor: move; } - label, a, .favicon { + label, a, .favicon, #qr img { cursor: pointer; } @@ -2366,6 +2422,12 @@ Main = .error { color: red; } + #error { + cursor: default; + } + #error[href] { + cursor: pointer; + } td.replyhider { vertical-align: top; } @@ -2447,6 +2509,47 @@ Main = width: 100%; } + #qr { + position: fixed; + max-height: 100%; + overflow-x: hidden; + overflow-y: auto; + } + #qr > div.move { + text-align: right; + } + #qr input[name=name] { + float: left; + } + #qr_form { + clear: left; + } + #qr_form, #qr #com_submit, #qr input[name=upfile] { + margin: 0; + } + #qr textarea { + width: 100%; + height: 125px; + } + #qr #close, #qr #autohide { + float: right; + } + #qr:not(:hover) > #autohide:checked ~ .autohide { + height: 0; + overflow: hidden; + } + /* http://stackoverflow.com/questions/2610497/change-an-inputs-html5-placeholder-color-with-css */ + #qr input::-webkit-input-placeholder { + color: grey; + } + #qr input:-moz-placeholder { + color: grey; + } + /* qr reCAPTCHA */ + #qr img { + border: 1px solid #AAA; + } + #updater { position: fixed; text-align: right; @@ -2518,102 +2621,6 @@ Main = #files > input { display: block; } - #qr { - position: fixed; - } - #qr .close, #qr #autohide { - float: right; - } - #qr > .move { - text-align: right; - } - #qr .autohide > input { - width: 90px; - } - #qr #autopost { - width: auto; - } - #qr #recaptcha_response_field { - width: 100%; - } - #qr form { - margin: 0; - } - #qr .autohide { - clear: both; - } - #qr:not(:hover) #autohide:checked ~ .autohide { - height: 0; - overflow: hidden; - } - #qr textarea { - border: 0; - height: 150px; - width: 100%; - } - #qr #captcha { - position: relative; - } - #qr #files { - width: 300px; - white-space: nowrap; - overflow: auto; - margin: 0; - padding: 0; - } - #qr #files li { - position: relative; - display: inline-block; - width: 100px; - height: 100px; - overflow: hidden; - } - #qr #files a { - position: absolute; - left: 0; - font-size: 50px; - color: red; - z-index: 1; - } - #qr #cl { - right: 0; - padding: 2px; - position: absolute; - } - #qr #files input { - /* cannot use `display: none;` - https://bugs.webkit.org/show_bug.cgi?id=58208 - http://code.google.com/p/chromium/issues/detail?id=78961 - */ - font-size: 100px; - opacity: 0; - } - #qr #files img { - position: absolute; - left: 0; - max-height: 100px; - max-width: 100px; - } - #qr input[name=resto] { - width: 80px; - } - #qr button + input[type=file] { - position: absolute; - opacity: 0; - pointer-events: none; - } - #qr .wat { - display: inline-block; - width: 16px; - overflow: hidden; - position: relative; - vertical-align: text-top; - } - #qr .wat input { - opacity: 0; - position: absolute; - left: 0; - } ' Main.init()