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.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
");
- 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.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
-
-
-
-
-
-
- "
- #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()