Put the old QR back; this is temporary.

This commit is contained in:
Nicolas Stepien 2011-11-26 04:25:06 +01:00
parent 847cef2ae7
commit 2bfc3e649c
2 changed files with 790 additions and 796 deletions

View File

@ -64,7 +64,7 @@
*/ */
(function() { (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; var __slice = Array.prototype.slice;
config = { config = {
@ -104,6 +104,7 @@
'Auto Watch Reply': [false, 'Automatically watch threads that you reply to'] 'Auto Watch Reply': [false, 'Automatically watch threads that you reply to']
}, },
Posting: { Posting: {
'Auto Noko': [true, 'Always redirect to your post'],
'Cooldown': [true, 'Prevent `flood detected` errors'], 'Cooldown': [true, 'Prevent `flood detected` errors'],
'Quick Reply': [true, 'Reply without leaving the page'], 'Quick Reply': [true, 'Reply without leaving the page'],
'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'], 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'],
@ -894,8 +895,8 @@
case conf.close: case conf.close:
if (o = $('#overlay')) { if (o = $('#overlay')) {
$.rm(o); $.rm(o);
} else if (QR.qr) { } else if (qr.qr) {
QR.close(); qr.close();
} }
break; break;
case conf.spoiler: case conf.spoiler:
@ -963,8 +964,8 @@
if ((_ref3 = $('input[value=Previous]')) != null) _ref3.click(); if ((_ref3 = $('input[value=Previous]')) != null) _ref3.click();
break; break;
case conf.submit: case conf.submit:
if (QR.qr) { if (qr.qr) {
QR.submit.call($('form', QR.qr)); qr.submit.call($('form', qr.qr));
} else { } else {
$('.postarea form').submit(); $('.postarea form').submit();
} }
@ -1060,12 +1061,12 @@
}, },
qr: function(thread, quote) { qr: function(thread, quote) {
if (quote) { if (quote) {
return QR.quote.call($('.quotejs + a', $('.replyhl', thread) || thread)); return qr.quote.call($('.quotejs + a', $('.replyhl', thread) || thread));
} else { } else {
if (QR.qr) { if (qr.qr) {
return $('textarea', QR.qr).focus(); return $('textarea', qr.qr).focus();
} else { } 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() { init: function() {
var holder; var match, time, _;
if (!($('form[name=post]') && $('#recaptcha_response_field'))) return; if (match = location.search.match(/cooldown=(\d+)/)) {
g.callbacks.push(function(root) { _ = match[0], time = match[1];
var quote; if ($.get(g.BOARD + '/cooldown', 0) < time) {
quote = $('.quotejs + a', root); $.set(g.BOARD + '/cooldown', time);
return $.on(quote, 'click', QR.quote); }
}
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', { if (g.REPLY) return $('.postarea form').action += '?cooldown';
name: 'iframe', },
hidden: true start: function() {
})); var submit, _i, _len, _ref;
$('#recaptcha_response_field').id = ''; cooldown.duration = Math.ceil(($.get(g.BOARD + '/cooldown', 0) - Date.now()) / 1000);
holder = $('#recaptcha_challenge_field_holder'); if (!(cooldown.duration > 0)) return;
$.on(holder, 'DOMNodeInserted', QR.captchaNode); _ref = $$('#com_submit');
QR.captchaNode({ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
target: holder.firstChild submit = _ref[_i];
}); submit.value = cooldown.duration;
QR.accept = $('.rules').textContent.match(/: (.+) /)[1].replace(/\w+/g, function(type) { 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') ? '<label> [<input type=checkbox name=spoiler>Spoiler Image?]</label>' : '';
qr.acceptFiles = $('.rules').textContent.match(/: (.+) /)[1].replace(/\w+/g, function(type) {
switch (type) { switch (type) {
case 'JPG': case 'JPG':
return 'image/JPEG'; return 'image/JPEG';
@ -1422,315 +1462,295 @@
return 'image/' + type; return 'image/' + type;
} }
}); });
QR.MAX_FILE_SIZE = $('input[name=MAX_FILE_SIZE]').value; iframe = $.el('iframe', {
QR.spoiler = $('.postarea label') ? ' <label>[<input type=checkbox name=spoiler>Spoiler Image?]</label>' : ''; name: 'iframe',
if (conf['Persistent QR']) { hidden: true
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: "<img><a class=x>X</a>"
}); });
$.on($('.x', box), 'click', QR.rmThumb); $.add(d.body, iframe);
$.add(box, file); return $('#recaptcha_response_field').id = '';
$.add(files, box);
QR.stats();
return QR.foo();
}, },
rmThumb: function() { attach: function() {
$.rm(this.parentNode); var fileDiv;
return QR.stats(); fileDiv = $.el('div', {
innerHTML: "<input type=file name=upfile accept='" + qr.acceptFiles + "'><a>X</a>"
});
$.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) { captchaNode: function(e) {
QR.captcha = { if (!qr.el) return;
challenge: e.target.value, val = e.target.value;
time: Date.now() $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + val;
}; qr.challenge = val;
return QR.captchaImg(); return qr.captchaTime = Date.now();
}, },
captchaImg: function() { captchaKeydown: function(e) {
var c, qr; var captchas;
qr = QR.qr; if (!(e.keyCode === 13 && this.value)) return;
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;
captchas = $.get('captchas', []); captchas = $.get('captchas', []);
captchas.push(captcha); captchas.push({
challenge: qr.challenge,
response: this.value,
time: qr.captchaTime
});
$.set('captchas', captchas); $.set('captchas', captchas);
el.value = ''; $('#captchas', qr.el).textContent = captchas.length + ' captchas';
QR.captchaReload(); Recaptcha.reload();
return QR.stats(captchas); this.value = '';
if (!$('textarea', qr.el).value && !$('input[type=file]', qr.el).files.length) {
return e.preventDefault();
}
}, },
captchaShift: function() { close: function() {
var captcha, captchas, cutoff; $.rm(qr.el);
captchas = $.get('captchas', []); 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 = " <a id=close title=close>X</a> <input type=checkbox id=autohide title=autohide> <div class=move> <input class=inputtext type=text name=name value='" + name + "' placeholder=Name form=qr_form> Quick Reply </div> <div class=autohide> <form name=post action=http://sys.4chan.org/" + g.BOARD + "/post method=POST enctype=multipart/form-data target=iframe id=qr_form> <input type=hidden name=resto value=" + THREAD_ID + "> <input type=hidden name=mode value=regist> <input type=hidden name=recaptcha_challenge_field id=recaptcha_challenge_field> <input type=hidden name=recaptcha_response_field id=recaptcha_response_field> <div><input class=inputtext type=text name=email value='" + email + "' placeholder=E-mail>" + qr.spoiler + "</div> <div><input class=inputtext type=text name=sub placeholder=Subject><input type=submit value=" + submitValue + " id=com_submit " + submitDisabled + "><label><input type=checkbox id=auto>auto</label></div> <div><textarea class=inputtext name=com placeholder=Comment></textarea></div> <div><img src=http://www.google.com/recaptcha/api/image?c=" + qr.challenge + "></div> <div><input class=inputtext type=text autocomplete=off placeholder=Verification id=dummy><span id=captchas>" + ($.get('captchas', []).length) + " captchas</span></div> <div><input type=file name=upfile accept='" + qr.acceptFiles + "'></div> </form> <div id=files></div> <div><input class=inputtext type=password name=pwd value='" + pwd + "' placeholder=Password form=qr_form maxlength=8><a id=attach>attach another file</a></div> </div> <a id=error class=error></a> ";
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; cutoff = Date.now() - 5 * HOUR + 5 * MINUTE;
captchas = $.get('captchas', []);
while (captcha = captchas.shift()) { while (captcha = captchas.shift()) {
if (captcha.time > cutoff) break; if (captcha.time > cutoff) break;
} }
$.set('captchas', captchas); $.set('captchas', captchas);
QR.stats(captchas); $('#captchas', qr.el).textContent = captchas.length + ' captchas';
return captcha; if (!captcha) {
}, dummy = $('#dummy', qr.el);
stats: function(captchas) { if (!(response = dummy.value)) {
var images, qr; return 'You forgot to type in the verification';
qr = QR.qr; }
captchas || (captchas = $.get('captchas', [])); captcha = {
images = $$('#files input', qr); challenge: qr.challenge,
return $('#qr_stats', qr).textContent = "" + images.length + " / " + captchas.length; response: response
}, };
captchaReload: function() { dummy.value = '';
return window.location = 'javascript:Recaptcha.reload()'; 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;
} }
if (this.parentNode.className === 'wat') QR.attach(this); $('#recaptcha_challenge_field', qr.el).value = captcha.challenge;
fr = new FileReader(); $('#recaptcha_response_field', qr.el).value = captcha.response;
img = $('img', this.parentNode); return false;
fr.onload = function(e) {
return img.src = e.target.result;
};
return fr.readAsDataURL(file);
}, },
close: function() { quote: function(e) {
$.rm(QR.qr); var id, s, selection, selectionID, ta, text, _ref;
return QR.qr = null; if (e) e.preventDefault();
}, if (qr.el) {
cooldown: function() { $('#autohide', qr.el).checked = false;
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);
} else { } else {
$.extend(b, { qr.dialog(this);
textContent: 'Submit',
disabled: false
});
if ($('#autopost', QR.qr).checked) return QR.submit();
} }
},
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;', " <a class=close>X</a> <input type=checkbox id=autohide title=autohide> <div class=move> <span id=qr_stats></span> </div> <div class=autohide> <span class=wat><img src=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41Ljg3O4BdAAAAXUlEQVQ4T2NgoAH4DzQTHyZoJckGENJASB6nc9GdCjdo6tSptkCsCPUqVgNAmtFtxiYGUkO0QrBibOqJtWkIGYDTqTgSGOnRiGYQ3mRLKBFhjUZiNCGrIZg3aKsAAGu4rTMFLFBMAAAAAElFTkSuQmCC></span> <input form=qr_form placeholder=Name name=name> <input form=qr_form placeholder=Email name=email> <input form=qr_form placeholder=Subject name=sub> <ul id=files></ul> <form enctype=multipart/form-data method=post action=http://sys.4chan.org/" + g.BOARD + "/post target=iframe id=qr_form> <textarea placeholder=Comment name=com></textarea> <div hidden> <input name=pwd> <input name=mode value=regist> <input name=recaptcha_challenge_field id=challenge> <input name=recaptcha_response_field id=response> </div> <div id=captcha> <div><img></div> <input id=recaptcha_response_field autocomplete=off> </div> <div> <button>Submit</button> " + (g.REPLY ? "<label>[<input type=checkbox id=autopost title=autopost> Autopost]</label>" : '') + " <input form=qr_form placeholder=Thread name=resto value=" + tid + " " + (g.REPLY ? 'hidden' : '') + "> " + QR.spoiler + " </div> </form> </div> <a class=error></a> ");
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; id = this.textContent;
text = ">>" + id + "\n"; text = ">>" + id + "\n";
sel = getSelection(); selection = window.getSelection();
bq = $.x('ancestor::blockquote', sel.anchorNode); if (s = selection.toString()) {
if (id === ((_ref2 = $.x('preceding-sibling::input', bq)) != null ? _ref2.name : void 0)) { selectionID = (_ref = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)) != null ? _ref.name : void 0;
if (s = sel.toString().replace(/\n/g, '\n>')) text += ">" + s + "\n"; if (selectionID === id) {
} s = s.replace(/\n/g, '\n>');
qr = QR.qr; text += ">" + s + "\n";
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);
} }
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() { refresh: function() {
var c, m, qr, _ref; var m, newFile, oldFile, _ref;
qr = QR.qr; $('[name=sub]', qr.el).value = '';
c = d.cookie; $('[name=email]', qr.el).value = (m = d.cookie.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : '';
$('[name=name]', qr).value = (m = c.match(/4chan_name=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; $('[name=com]', qr.el).value = '';
$('[name=email]', qr).value = (m = c.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; $('[name=recaptcha_response_field]', qr.el).value = '';
$('[name=pwd]', qr).value = (m = c.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value;
$('[name=sub]', qr).value = '';
if (!conf['Remember Spoiler']) { 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) { submit: function(e) {
var captcha, challenge, el, id, input, op, qr, response; var id, msg, op;
qr = QR.qr; if (msg = qr.postInvalid()) {
if ($('textarea', qr).value || $('#files', qr).childNodes.length) { if (typeof e.preventDefault === "function") e.preventDefault();
if ($('form button', qr).disabled) { alert(msg);
$('#autopost', qr).checked = true; if (msg === 'You forgot to type in the verification.') {
return; $('#dummy', qr.el).focus();
}
} else {
if (e) {
alert('Error: No text entered.');
e.preventDefault();
} }
return; return;
} }
$('.error', qr).textContent = ''; if (conf['Auto Watch Reply'] && conf['Thread Watcher']) {
if (e && (el = $('#recaptcha_response_field', qr)).value) QR.captchaPush(el); if (g.REPLY && $('img.favicon').src === Favicon.empty) {
if (!(captcha = QR.captchaShift())) { watcher.watch(null, g.THREAD_ID);
alert('You forgot to type in the verification.'); } else {
if (e != null) e.preventDefault(); id = $('input[name=resto]', qr.el).value;
return; op = $.id(id);
} if ($('img.favicon', op).src === Favicon.empty) watcher.watch(op, id);
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 (!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() { sys: function() {
var recaptcha; var c, duration, id, noko, recaptcha, sage, search, thread, url, watch, _, _ref, _ref2;
$.off(d, 'DOMContentLoaded', QR.sys);
if (recaptcha = $('#recaptcha_response_field')) { if (recaptcha = $('#recaptcha_response_field')) {
$.on(recaptcha, 'keydown', QR.keydown); $.on(recaptcha, 'keydown', Recaptcha.listener);
return; return;
} }
/* /*
http://code.google.com/p/chromium/issues/detail?id=20773 http://code.google.com/p/chromium/issues/detail?id=20773
Let content scripts see other frames (instead of them being undefined) 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 To access the parent, we have to break out of the sandbox and evaluate
in the global context. in the global context.
*/ */
return $.globalEval(function() { $.globalEval(function() {
var data, href, node, textContent, _ref; var data, node, _ref;
$ = function(css) { data = {
return document.querySelector(css); to: 'qr.message'
}; };
if (node = (_ref = $('table font b')) != null ? _ref.firstChild : void 0) { if (node = (_ref = document.querySelector('table font b')) != null ? _ref.firstChild : void 0) {
textContent = node.textContent, href = node.href; data.textContent = node.textContent;
} else { data.href = node.href;
node = $('meta');
href = node.content.match(/url=(.+)/)[1];
} }
data = JSON.stringify({
textContent: textContent,
href: href
});
return parent.postMessage(data, '*'); 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 = { Main = {
init: function() { init: function() {
var cutoff, hiddenThreads, id, lastChecked, now, pathname, temp, timestamp, _ref; 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('/'); pathname = location.pathname.substring(1).split('/');
g.BOARD = pathname[0], temp = pathname[1]; g.BOARD = pathname[0], temp = pathname[1];
if (temp === 'res') { if (temp === 'res') {
@ -2889,6 +2900,15 @@
} else { } else {
g.PAGENUM = parseInt(temp) || 0; 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 + "/", {}); g.hiddenReplies = $.get("hiddenReplies/" + g.BOARD + "/", {});
lastChecked = $.get('lastChecked', 0); lastChecked = $.get('lastChecked', 0);
now = Date.now(); now = Date.now();
@ -2929,7 +2949,7 @@
} }
}, },
onLoad: function() { 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); $.off(d, 'DOMContentLoaded', Main.onLoad);
if (conf['404 Redirect'] && d.title === '4chan - 404' && /^\d+$/.test(g.THREAD_ID)) { if (conf['404 Redirect'] && d.title === '4chan - 404' && /^\d+$/.test(g.THREAD_ID)) {
redirect(); redirect();
@ -2939,9 +2959,21 @@
$.addStyle(Main.css); $.addStyle(Main.css);
threading.init(); threading.init();
Favicon.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['Image Expansion']) imgExpand.init();
if (conf['Reveal Spoilers'] && $('.postarea label')) revealSpoilers.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['Thread Watcher']) watcher.init();
if (conf['Keybinds']) keybinds.init(); if (conf['Keybinds']) keybinds.init();
if (g.REPLY) { if (g.REPLY) {
@ -2950,6 +2982,10 @@
if (conf['Reply Navigation']) nav.init(); if (conf['Reply Navigation']) nav.init();
if (conf['Post in Title']) titlePost.init(); if (conf['Post in Title']) titlePost.init();
if (conf['Unread Count']) unread.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 { } else {
if (conf['Thread Hiding']) threadHiding.init(); if (conf['Thread Hiding']) threadHiding.init();
if (conf['Thread Expansion']) expandThread.init(); if (conf['Thread Expansion']) expandThread.init();
@ -2975,7 +3011,7 @@
message: function(e) { message: function(e) {
var data, origin; var data, origin;
origin = e.origin, data = e.data; 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) { node: function(e) {
var callback, target, _i, _len, _ref, _results; var callback, target, _i, _len, _ref, _results;
@ -3001,7 +3037,7 @@
div.dialog > div.move {\ div.dialog > div.move {\
cursor: move;\ cursor: move;\
}\ }\
label, a, .favicon {\ label, a, .favicon, #qr img {\
cursor: pointer;\ cursor: pointer;\
}\ }\
\ \
@ -3011,6 +3047,12 @@
.error {\ .error {\
color: red;\ color: red;\
}\ }\
#error {\
cursor: default;\
}\
#error[href] {\
cursor: pointer;\
}\
td.replyhider {\ td.replyhider {\
vertical-align: top;\ vertical-align: top;\
}\ }\
@ -3091,6 +3133,47 @@
resize: vertical;\ resize: vertical;\
width: 100%;\ 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 {\ #updater {\
position: fixed;\ position: fixed;\
@ -3163,102 +3246,6 @@
#files > input {\ #files > input {\
display: block;\ 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;\
}\
' '
}; };

View File

@ -31,6 +31,7 @@ config =
'Auto Watch': [true, 'Automatically watch threads that you start'] 'Auto Watch': [true, 'Automatically watch threads that you start']
'Auto Watch Reply': [false, 'Automatically watch threads that you reply to'] 'Auto Watch Reply': [false, 'Automatically watch threads that you reply to']
Posting: Posting:
'Auto Noko': [true, 'Always redirect to your post']
'Cooldown': [true, 'Prevent `flood detected` errors'] 'Cooldown': [true, 'Prevent `flood detected` errors']
'Quick Reply': [true, 'Reply without leaving the page'] 'Quick Reply': [true, 'Reply without leaving the page']
'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'] 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.']
@ -642,8 +643,8 @@ keybinds =
when conf.close when conf.close
if o = $ '#overlay' if o = $ '#overlay'
$.rm o $.rm o
else if QR.qr else if qr.qr
QR.close() qr.close()
when conf.spoiler when conf.spoiler
ta = e.target ta = e.target
return unless ta.nodeName is 'TEXTAREA' return unless ta.nodeName is 'TEXTAREA'
@ -694,8 +695,8 @@ keybinds =
when conf.previousPage when conf.previousPage
$('input[value=Previous]')?.click() $('input[value=Previous]')?.click()
when conf.submit when conf.submit
if QR.qr if qr.qr
QR.submit.call $ 'form', QR.qr qr.submit.call $ 'form', qr.qr
else else
$('.postarea form').submit() $('.postarea form').submit()
when conf.unreadCountTo0 when conf.unreadCountTo0
@ -740,12 +741,12 @@ keybinds =
qr: (thread, quote) -> qr: (thread, quote) ->
if quote if quote
QR.quote.call $ '.quotejs + a', $('.replyhl', thread) or thread qr.quote.call $ '.quotejs + a', $('.replyhl', thread) or thread
else else
if QR.qr if qr.qr
$('textarea', QR.qr).focus() $('textarea', qr.qr).focus()
else else
QR.dialog '', thread?.firstChild.id qr.dialog '', thread?.firstChild.id
open: (thread, tab) -> open: (thread, tab) ->
id = thread.firstChild.id id = thread.firstChild.id
@ -1033,25 +1034,49 @@ options =
conf['backlink'] = @value conf['backlink'] = @value
$('#backlinkPreview').textContent = conf['backlink'].replace /%id/, '123456789' $('#backlinkPreview').textContent = conf['backlink'].replace /%id/, '123456789'
QR = cooldown =
#captcha caching for report form #TODO merge into qr
#report queueing
#check if captchas can be reused on eg dup file error
init: -> init: ->
#can't reply in some stickies, recaptcha may be blocked, eg by noscript if match = location.search.match /cooldown=(\d+)/
return unless $('form[name=post]') and $('#recaptcha_response_field') [_, time] = match
g.callbacks.push (root) -> $.set g.BOARD+'/cooldown', time if $.get(g.BOARD+'/cooldown', 0) < time
quote = $ '.quotejs + a', root cooldown.start() if Date.now() < $.get g.BOARD+'/cooldown', 0
$.on quote, 'click', QR.quote $.on window, 'storage', (e) -> cooldown.start() if e.key is "#{NAMESPACE}#{g.BOARD}/cooldown"
$.add d.body, $.el 'iframe', $('.postarea form').action += '?cooldown' if g.REPLY
name: 'iframe'
hidden: true start: ->
# nuke id so qr's field focuses on recaptcha reload, instead of normal form's cooldown.duration = Math.ceil ($.get(g.BOARD+'/cooldown', 0) - Date.now()) / 1000
$('#recaptcha_response_field').id = '' return unless cooldown.duration > 0
holder = $ '#recaptcha_challenge_field_holder' for submit in $$ '#com_submit'
$.on holder, 'DOMNodeInserted', QR.captchaNode submit.value = cooldown.duration
QR.captchaNode target: holder.firstChild submit.disabled = true
QR.accept = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) -> 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 '<label> [<input type=checkbox name=spoiler>Spoiler Image?]</label>' else ''
qr.acceptFiles = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) ->
switch type switch type
when 'JPG' when 'JPG'
'image/JPEG' 'image/JPEG'
@ -1059,281 +1084,295 @@ QR =
'application/' + type 'application/' + type
else else
'image/' + type 'image/' + type
QR.MAX_FILE_SIZE = $('input[name=MAX_FILE_SIZE]').value
QR.spoiler = if $('.postarea label') then ' <label>[<input type=checkbox name=spoiler>Spoiler Image?]</label>' else '' iframe = $.el 'iframe',
if conf['Persistent QR'] name: 'iframe'
QR.dialog() hidden: true
$('textarea', QR.qr).blur() $.add d.body, iframe
if conf['Auto Hide QR']
$('#autohide', QR.qr).checked = true #hack - nuke id so it doesn't grab focus when reloading
if conf['Cooldown'] $('#recaptcha_response_field').id = ''
$.on window, 'storage', (e) -> QR.cooldown() if e.key is "#{NAMESPACE}cooldown/#{g.BOARD}"
attach: (file) -> attach: ->
files = $ '#files', QR.qr fileDiv = $.el 'div', innerHTML: "<input type=file name=upfile accept='#{qr.acceptFiles}'><a>X</a>"
box = $.el 'li', $.on fileDiv.firstChild, 'change', qr.validateFileSize
innerHTML: "<img><a class=x>X</a>" $.on fileDiv.lastChild, 'click', (-> $.rm @parentNode)
$.on $('.x', box), 'click', QR.rmThumb $.add $('#files', qr.el), fileDiv
$.add box, file
$.add files, box attachNext: ->
QR.stats() fileDiv = $.rm $('#files div', qr.el)
QR.foo() file = fileDiv.firstChild
rmThumb: -> oldFile = $ '#qr_form input[type=file]', qr.el
$.rm @parentNode $.replace oldFile, file
QR.stats()
autoPost: ->
if qr.el and $('#auto', qr.el).checked
qr.submit.call $ 'form', qr.el
captchaNode: (e) -> captchaNode: (e) ->
QR.captcha = return unless qr.el
challenge: e.target.value val = e.target.value
time: Date.now() $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + val
QR.captchaImg() qr.challenge = val
captchaImg: -> qr.captchaTime = Date.now()
{qr} = QR
return unless qr captchaKeydown: (e) ->
c = QR.captcha.challenge return unless e.keyCode is 13 and @value #enter, captcha filled
$('#captcha img', qr).src = "http://www.google.com/recaptcha/api/image?c=#{c}"
captchaPush: (el) ->
{captcha} = QR
captcha.response = el.value
captchas = $.get 'captchas', [] captchas = $.get 'captchas', []
captchas.push captcha captchas.push
challenge: qr.challenge
response: @value
time: qr.captchaTime
$.set 'captchas', captchas $.set 'captchas', captchas
el.value = '' $('#captchas', qr.el).textContent = captchas.length + ' captchas'
QR.captchaReload() Recaptcha.reload()
QR.stats captchas @value = ''
captchaShift: ->
captchas = $.get 'captchas', [] 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 = "
<a id=close title=close>X</a>
<input type=checkbox id=autohide title=autohide>
<div class=move>
<input class=inputtext type=text name=name value='#{name}' placeholder=Name form=qr_form>
Quick Reply
</div>
<div class=autohide>
<form name=post action=http://sys.4chan.org/#{g.BOARD}/post method=POST enctype=multipart/form-data target=iframe id=qr_form>
<input type=hidden name=resto value=#{THREAD_ID}>
<input type=hidden name=mode value=regist>
<input type=hidden name=recaptcha_challenge_field id=recaptcha_challenge_field>
<input type=hidden name=recaptcha_response_field id=recaptcha_response_field>
<div><input class=inputtext type=text name=email value='#{email}' placeholder=E-mail>#{qr.spoiler}</div>
<div><input class=inputtext type=text name=sub placeholder=Subject><input type=submit value=#{submitValue} id=com_submit #{submitDisabled}><label><input type=checkbox id=auto>auto</label></div>
<div><textarea class=inputtext name=com placeholder=Comment></textarea></div>
<div><img src=http://www.google.com/recaptcha/api/image?c=#{qr.challenge}></div>
<div><input class=inputtext type=text autocomplete=off placeholder=Verification id=dummy><span id=captchas>#{$.get('captchas', []).length} captchas</span></div>
<div><input type=file name=upfile accept='#{qr.acceptFiles}'></div>
</form>
<div id=files></div>
<div><input class=inputtext type=password name=pwd value='#{pwd}' placeholder=Password form=qr_form maxlength=8><a id=attach>attach another file</a></div>
</div>
<a id=error class=error></a>
"
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 cutoff = Date.now() - 5*HOUR + 5*MINUTE
captchas = $.get 'captchas', []
while captcha = captchas.shift() while captcha = captchas.shift()
if captcha.time > cutoff if captcha.time > cutoff
break break
$.set 'captchas', captchas $.set 'captchas', captchas
QR.stats captchas
captcha $('#captchas', qr.el).textContent = captchas.length + ' captchas'
stats: (captchas) ->
{qr} = QR unless captcha
captchas or= $.get 'captchas', [] dummy = $ '#dummy', qr.el
images = $$ '#files input', qr return 'You forgot to type in the verification' unless response = dummy.value
$('#qr_stats', qr).textContent = "#{images.length} / #{captchas.length}" captcha =
captchaReload: -> challenge: qr.challenge
window.location = 'javascript:Recaptcha.reload()' response: response
change: (e) -> dummy.value = ''
file = @files[0] Recaptcha.reload()
if file.size > QR.MAX_FILE_SIZE
alert 'Error: File too large.' $('#recaptcha_challenge_field', qr.el).value = captcha.challenge
QR.foo @ $('#recaptcha_response_field', qr.el).value = captcha.response
return
if @parentNode.className is 'wat' false
QR.attach @
fr = new FileReader() quote: (e) ->
img = $ 'img', @parentNode e.preventDefault() if e
fr.onload = (e) ->
img.src = e.target.result if qr.el
fr.readAsDataURL file $('#autohide', qr.el).checked = false
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
else else
$.extend b, qr.dialog @
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;', "
<a class=close>X</a>
<input type=checkbox id=autohide title=autohide>
<div class=move>
<span id=qr_stats></span>
</div>
<div class=autohide>
<span class=wat><img src=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41Ljg3O4BdAAAAXUlEQVQ4T2NgoAH4DzQTHyZoJckGENJASB6nc9GdCjdo6tSptkCsCPUqVgNAmtFtxiYGUkO0QrBibOqJtWkIGYDTqTgSGOnRiGYQ3mRLKBFhjUZiNCGrIZg3aKsAAGu4rTMFLFBMAAAAAElFTkSuQmCC></span>
<input form=qr_form placeholder=Name name=name>
<input form=qr_form placeholder=Email name=email>
<input form=qr_form placeholder=Subject name=sub>
<ul id=files></ul>
<form enctype=multipart/form-data method=post action=http://sys.4chan.org/#{g.BOARD}/post target=iframe id=qr_form>
<textarea placeholder=Comment name=com></textarea>
<div hidden>
<input name=pwd>
<input name=mode value=regist>
<input name=recaptcha_challenge_field id=challenge>
<input name=recaptcha_response_field id=response>
</div>
<div id=captcha>
<div><img></div>
<input id=recaptcha_response_field autocomplete=off>
</div>
<div>
<button>Submit</button>
#{if g.REPLY then "<label>[<input type=checkbox id=autopost title=autopost> Autopost]</label>" else ''}
<input form=qr_form placeholder=Thread name=resto value=#{tid} #{if g.REPLY then 'hidden' else ''}>
#{QR.spoiler}
</div>
</form>
</div>
<a class=error></a>
"
#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
id = @textContent id = @textContent
text = ">>#{id}\n" 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 selection = window.getSelection()
in the global context. 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 -> $.globalEval ->
$ = (css) -> document.querySelector css data = to: 'qr.message'
if node = $('table font b')?.firstChild if node = document.querySelector('table font b')?.firstChild
{textContent, href} = node data.textContent = node.textContent
else data.href = node.href
node = $ 'meta'
href = node.content.match(/url=(.+)/)[1]
data = JSON.stringify { textContent, href }
parent.postMessage data, '*' 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 = threading =
init: -> init: ->
@ -2179,15 +2218,6 @@ imgExpand =
Main = Main =
init: -> 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('/') pathname = location.pathname.substring(1).split('/')
[g.BOARD, temp] = pathname [g.BOARD, temp] = pathname
if temp is 'res' if temp is 'res'
@ -2196,6 +2226,15 @@ Main =
else else
g.PAGENUM = parseInt(temp) or 0 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}/", {} g.hiddenReplies = $.get "hiddenReplies/#{g.BOARD}/", {}
lastChecked = $.get 'lastChecked', 0 lastChecked = $.get 'lastChecked', 0
@ -2277,8 +2316,20 @@ Main =
threading.init() threading.init()
Favicon.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 #major features
if conf['Auto Noko'] and canPost
form.action += '?noko'
if conf['Cooldown'] and canPost
cooldown.init()
if conf['Image Expansion'] if conf['Image Expansion']
imgExpand.init() imgExpand.init()
@ -2286,7 +2337,7 @@ Main =
revealSpoilers.init() revealSpoilers.init()
if conf['Quick Reply'] if conf['Quick Reply']
QR.init() qr.init()
if conf['Thread Watcher'] if conf['Thread Watcher']
watcher.init() watcher.init()
@ -2310,6 +2361,11 @@ Main =
if conf['Unread Count'] if conf['Unread Count']
unread.init() 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 else #not reply
if conf['Thread Hiding'] if conf['Thread Hiding']
threadHiding.init() threadHiding.init()
@ -2337,7 +2393,7 @@ Main =
message: (e) -> message: (e) ->
{origin, data} = e {origin, data} = e
if origin is 'http://sys.4chan.org' if origin is 'http://sys.4chan.org'
QR.receive data qr.message data
node: (e) -> node: (e) ->
{target} = e {target} = e
@ -2356,7 +2412,7 @@ Main =
div.dialog > div.move { div.dialog > div.move {
cursor: move; cursor: move;
} }
label, a, .favicon { label, a, .favicon, #qr img {
cursor: pointer; cursor: pointer;
} }
@ -2366,6 +2422,12 @@ Main =
.error { .error {
color: red; color: red;
} }
#error {
cursor: default;
}
#error[href] {
cursor: pointer;
}
td.replyhider { td.replyhider {
vertical-align: top; vertical-align: top;
} }
@ -2447,6 +2509,47 @@ Main =
width: 100%; 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 { #updater {
position: fixed; position: fixed;
text-align: right; text-align: right;
@ -2518,102 +2621,6 @@ Main =
#files > input { #files > input {
display: block; 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() Main.init()