diff --git a/4chan_x.user.js b/4chan_x.user.js
index be96638a6..9dd93a271 100644
--- a/4chan_x.user.js
+++ b/4chan_x.user.js
@@ -1821,939 +1821,6 @@
}
};
- QR = {
- init: function() {
- var link;
- if (g.VIEW === 'catalog' || !Conf['Quick Reply']) {
- return;
- }
- if (Conf['Hide Original Post Form']) {
- $.addClass(doc, 'hide-original-post-form');
- }
- link = $.el('a', {
- className: 'qr-shortcut',
- textContent: 'Quick Reply',
- href: 'javascript:;'
- });
- $.on(link, 'click', function() {
- Header.menu.close();
- QR.open();
- if (g.BOARD.ID === 'f') {
- if (g.VIEW === 'index') {
- QR.threadSelector.value = '9999';
- }
- } else if (g.VIEW === 'thread') {
- QR.threadSelector.value = g.THREAD;
- } else {
- QR.threadSelector.value = 'new';
- }
- return $('textarea', QR.el).focus();
- });
- $.event('AddMenuEntry', {
- type: 'header',
- el: link
- });
- $.on(d, 'dragover', QR.dragOver);
- $.on(d, 'drop', QR.dropFile);
- $.on(d, 'dragstart dragend', QR.drag);
- $.on(d, '4chanXInitFinished', function() {
- if (!Conf['Persistent QR']) {
- return;
- }
- QR.open();
- if (Conf['Auto Hide QR']) {
- return QR.hide();
- }
- });
- return Post.prototype.callbacks.push({
- name: 'Quick Reply',
- cb: this.node
- });
- },
- node: function() {
- return $.on($('a[title="Quote this post"]', this.nodes.info), 'click', QR.quote);
- },
- open: function() {
- if (QR.el) {
- QR.el.hidden = false;
- QR.unhide();
- return;
- }
- try {
- return QR.dialog();
- } catch (err) {
- delete QR.el;
- return Main.handleErrors({
- message: 'Quick Reply dialog creation crashed.',
- error: err
- });
- }
- },
- close: function() {
- var i, spoiler, _i, _len, _ref;
- QR.el.hidden = true;
- QR.abort();
- d.activeElement.blur();
- $.rmClass(QR.el, 'dump');
- _ref = QR.replies;
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- i = _ref[_i];
- QR.replies[0].rm();
- }
- QR.cooldown.auto = false;
- QR.status();
- QR.resetFileInput();
- if (!Conf['Remember Spoiler'] && (spoiler = $.id('spoiler')).checked) {
- spoiler.click();
- }
- return QR.cleanNotification();
- },
- hide: function() {
- d.activeElement.blur();
- $.addClass(QR.el, 'autohide');
- return $.id('autohide').checked = true;
- },
- unhide: function() {
- $.rmClass(QR.el, 'autohide');
- return $.id('autohide').checked = false;
- },
- toggleHide: function() {
- if (this.checked) {
- return QR.hide();
- } else {
- return QR.unhide();
- }
- },
- error: function(err) {
- var el;
- QR.open();
- if (typeof err === 'string') {
- el = $.tn(err);
- } else {
- el = err;
- el.removeAttribute('style');
- }
- if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
- $('[autocomplete]', QR.el).focus();
- }
- if (d.hidden) {
- alert(el.textContent);
- }
- return QR.lastNotification = new Notification('warning', el);
- },
- cleanNotification: function() {
- var _ref;
- if ((_ref = QR.lastNotification) != null) {
- _ref.close();
- }
- return delete QR.lastNotification;
- },
- status: function(data) {
- var disabled, input, value;
- if (data == null) {
- data = {};
- }
- if (!QR.el) {
- return;
- }
- if (g.dead) {
- value = 404;
- disabled = true;
- QR.cooldown.auto = false;
- }
- value = data.progress || QR.cooldown.seconds || value;
- input = QR.status.input;
- input.value = QR.cooldown.auto ? value ? "Auto " + value : 'Auto' : value || 'Submit';
- return input.disabled = disabled || false;
- },
- cooldown: {
- init: function() {
- QR.cooldown.types = {
- thread: (function() {
- switch (g.BOARD) {
- case 'q':
- return 86400;
- case 'b':
- case 'soc':
- case 'r9k':
- return 600;
- default:
- return 300;
- }
- })(),
- sage: g.BOARD === 'q' ? 600 : 60,
- file: g.BOARD === 'q' ? 300 : 30,
- post: g.BOARD === 'q' ? 60 : 30
- };
- QR.cooldown.cooldowns = $.get("" + g.BOARD + ".cooldown", {});
- QR.cooldown.start();
- return $.sync("" + g.BOARD + ".cooldown", QR.cooldown.sync);
- },
- start: function() {
- if (QR.cooldown.isCounting) {
- return;
- }
- QR.cooldown.isCounting = true;
- return QR.cooldown.count();
- },
- sync: function(cooldowns) {
- var id;
- for (id in cooldowns) {
- QR.cooldown.cooldowns[id] = cooldowns[id];
- }
- return QR.cooldown.start();
- },
- set: function(data) {
- var cooldown, hasFile, isReply, isSage, start, type;
- start = Date.now();
- if (data.delay) {
- cooldown = {
- delay: data.delay
- };
- } else {
- isSage = /sage/i.test(data.post.email);
- hasFile = !!data.post.file;
- isReply = data.isReply;
- type = !isReply ? 'thread' : isSage ? 'sage' : hasFile ? 'file' : 'post';
- cooldown = {
- isReply: isReply,
- isSage: isSage,
- hasFile: hasFile,
- timeout: start + QR.cooldown.types[type] * $.SECOND
- };
- }
- QR.cooldown.cooldowns[start] = cooldown;
- $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns);
- return QR.cooldown.start();
- },
- unset: function(id) {
- delete QR.cooldown.cooldowns[id];
- return $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns);
- },
- count: function() {
- var cooldown, cooldowns, elapsed, hasFile, isReply, isSage, now, post, seconds, start, type, types, update, _ref;
- if (Object.keys(QR.cooldown.cooldowns).length) {
- setTimeout(QR.cooldown.count, 1000);
- } else {
- $["delete"]("" + g.BOARD + ".cooldown");
- delete QR.cooldown.isCounting;
- delete QR.cooldown.seconds;
- QR.status();
- return;
- }
- isReply = g.BOARD.ID === 'f' && g.VIEW === 'thread' ? true : QR.threadSelector.value !== 'new';
- if (isReply) {
- post = QR.replies[0];
- isSage = /sage/i.test(post.email);
- hasFile = !!post.file;
- }
- now = Date.now();
- seconds = null;
- _ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns;
- for (start in cooldowns) {
- cooldown = cooldowns[start];
- if ('delay' in cooldown) {
- if (cooldown.delay) {
- seconds = Math.max(seconds, cooldown.delay--);
- } else {
- seconds = Math.max(seconds, 0);
- QR.cooldown.unset(start);
- }
- continue;
- }
- if (isReply === cooldown.isReply) {
- type = !isReply ? 'thread' : isSage && cooldown.isSage ? 'sage' : hasFile && cooldown.hasFile ? 'file' : 'post';
- elapsed = Math.floor((now - start) / 1000);
- if (elapsed >= 0) {
- seconds = Math.max(seconds, types[type] - elapsed);
- }
- }
- if (!((start <= now && now <= cooldown.timeout))) {
- QR.cooldown.unset(start);
- }
- }
- update = seconds !== null || !!QR.cooldown.seconds;
- QR.cooldown.seconds = seconds;
- if (update) {
- QR.status();
- }
- if (seconds === 0 && QR.cooldown.auto) {
- return QR.submit();
- }
- }
- },
- quote: function(e) {
- var caretPos, post, range, s, sel, selectionRoot, ta, text;
- if (e != null) {
- e.preventDefault();
- }
- QR.open();
- ta = $('textarea', QR.el);
- if (QR.threadSelector && !ta.value && g.BOARD.ID !== 'f') {
- QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', this).id.slice(1);
- }
- post = Get.postFromRoot($.x('ancestor-or-self::div[contains(@class,"postContainer")][1]', this));
- text = ">>" + post + "\n";
- sel = d.getSelection();
- selectionRoot = $.x('ancestor-or-self::div[contains(@class,"postContainer")][1]', sel.anchorNode);
- if ((s = sel.toString().trim()) && post.nodes.root === selectionRoot) {
- s = s.replace(/\n/g, '\n>');
- text += ">" + s + "\n";
- }
- caretPos = ta.selectionStart;
- ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd);
- range = caretPos + text.length;
- ta.setSelectionRange(range, range);
- ta.focus();
- return ta.dispatchEvent(new Event('input'));
- },
- characterCount: function() {
- var count, counter;
- counter = QR.charaCounter;
- count = this.textLength;
- counter.textContent = count;
- counter.hidden = count < 1000;
- return (count > 1500 ? $.addClass : $.rmClass)(counter, 'warning');
- },
- drag: function(e) {
- var toggle;
- toggle = e.type === 'dragstart' ? $.off : $.on;
- toggle(d, 'dragover', QR.dragOver);
- return toggle(d, 'drop', QR.dropFile);
- },
- dragOver: function(e) {
- e.preventDefault();
- return e.dataTransfer.dropEffect = 'copy';
- },
- dropFile: function(e) {
- if (!e.dataTransfer.files.length) {
- return;
- }
- e.preventDefault();
- QR.open();
- QR.fileInput.call(e.dataTransfer);
- return $.addClass(QR.el, 'dump');
- },
- fileInput: function() {
- var file, _i, _len, _ref;
- QR.cleanNotification();
- if (this.files.length === 1) {
- file = this.files[0];
- if (file.size > this.max) {
- QR.error('File too large.');
- QR.resetFileInput();
- } else if (-1 === QR.mimeTypes.indexOf(file.type)) {
- QR.error('Unsupported file type.');
- QR.resetFileInput();
- } else {
- QR.selected.setFile(file);
- }
- return;
- }
- _ref = this.files;
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- file = _ref[_i];
- if (file.size > this.max) {
- QR.error("File " + file.name + " is too large.");
- break;
- } else if (-1 === QR.mimeTypes.indexOf(file.type)) {
- QR.error("" + file.name + ": Unsupported file type.");
- break;
- }
- if (!QR.replies[QR.replies.length - 1].file) {
- QR.replies[QR.replies.length - 1].setFile(file);
- } else {
- new QR.reply().setFile(file);
- }
- }
- $.addClass(QR.el, 'dump');
- return QR.resetFileInput();
- },
- resetFileInput: function() {
- return $('[type=file]', QR.el).value = null;
- },
- replies: [],
- reply: (function() {
-
- function _Class() {
- var persona, prev,
- _this = this;
- prev = QR.replies[QR.replies.length - 1];
- persona = $.get('QR.persona', {});
- this.name = prev ? prev.name : persona.name || null;
- this.email = prev && !/^sage$/.test(prev.email) ? prev.email : persona.email || null;
- this.sub = prev && Conf['Remember Subject'] ? prev.sub : Conf['Remember Subject'] ? persona.sub : null;
- this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false;
- this.com = null;
- this.el = $.el('a', {
- className: 'qrpreview',
- draggable: true,
- href: 'javascript:;',
- innerHTML: '×'
- });
- $('input', this.el).checked = this.spoiler;
- $.on(this.el, 'click', function() {
- return _this.select();
- });
- $.on($('.remove', this.el), 'click', function(e) {
- e.stopPropagation();
- return _this.rm();
- });
- $.on($('label', this.el), 'click', function(e) {
- return e.stopPropagation();
- });
- $.on($('input', this.el), 'change', function(e) {
- _this.spoiler = e.target.checked;
- if (_this.el.id === 'selected') {
- return $.id('spoiler').checked = _this.spoiler;
- }
- });
- $.before($('#addReply', QR.el), this.el);
- $.on(this.el, 'dragstart', this.dragStart);
- $.on(this.el, 'dragenter', this.dragEnter);
- $.on(this.el, 'dragleave', this.dragLeave);
- $.on(this.el, 'dragover', this.dragOver);
- $.on(this.el, 'dragend', this.dragEnd);
- $.on(this.el, 'drop', this.drop);
- QR.replies.push(this);
- }
-
- _Class.prototype.setFile = function(file) {
- var fileUrl, img, url,
- _this = this;
- this.file = file;
- this.el.title = "" + file.name + " (" + ($.bytesToString(file.size)) + ")";
- if (QR.spoiler) {
- $('label', this.el).hidden = false;
- }
- if (!/^image/.test(file.type)) {
- this.el.style.backgroundImage = null;
- return;
- }
- if (!(url = window.URL || window.webkitURL)) {
- return;
- }
- url.revokeObjectURL(this.url);
- fileUrl = url.createObjectURL(file);
- img = $.el('img');
- $.on(img, 'load', function() {
- var c, data, i, l, s, ui8a, _i;
- s = 90 * 3;
- if (img.height < s || img.width < s) {
- _this.url = fileUrl;
- _this.el.style.backgroundImage = "url(" + _this.url + ")";
- return;
- }
- if (img.height <= img.width) {
- img.width = s / img.height * img.width;
- img.height = s;
- } else {
- img.height = s / img.width * img.height;
- img.width = s;
- }
- c = $.el('canvas');
- c.height = img.height;
- c.width = img.width;
- c.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
- data = atob(c.toDataURL().split(',')[1]);
- l = data.length;
- ui8a = new Uint8Array(l);
- for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) {
- ui8a[i] = data.charCodeAt(i);
- }
- _this.url = url.createObjectURL(new Blob([ui8a], {
- type: 'image/png'
- }));
- _this.el.style.backgroundImage = "url(" + _this.url + ")";
- return typeof url.revokeObjectURL === "function" ? url.revokeObjectURL(fileUrl) : void 0;
- });
- return img.src = fileUrl;
- };
-
- _Class.prototype.rmFile = function() {
- var _base;
- QR.resetFileInput();
- delete this.file;
- this.el.title = null;
- this.el.style.backgroundImage = null;
- if (QR.spoiler) {
- $('label', this.el).hidden = true;
- }
- return typeof (_base = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base.revokeObjectURL(this.url) : void 0;
- };
-
- _Class.prototype.select = function() {
- var data, rectEl, rectList, _i, _len, _ref, _ref1;
- if ((_ref = QR.selected) != null) {
- _ref.el.id = null;
- }
- QR.selected = this;
- this.el.id = 'selected';
- rectEl = this.el.getBoundingClientRect();
- rectList = this.el.parentNode.getBoundingClientRect();
- this.el.parentNode.scrollLeft += rectEl.left + rectEl.width / 2 - rectList.left - rectList.width / 2;
- _ref1 = ['name', 'email', 'sub', 'com'];
- for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
- data = _ref1[_i];
- $("[name=" + data + "]", QR.el).value = this[data];
- }
- QR.characterCount.call($('textarea', QR.el));
- return $('#spoiler', QR.el).checked = this.spoiler;
- };
-
- _Class.prototype.dragStart = function() {
- return $.addClass(this, 'drag');
- };
-
- _Class.prototype.dragEnter = function() {
- return $.addClass(this, 'over');
- };
-
- _Class.prototype.dragLeave = function() {
- return $.rmClass(this, 'over');
- };
-
- _Class.prototype.dragOver = function(e) {
- e.preventDefault();
- return e.dataTransfer.dropEffect = 'move';
- };
-
- _Class.prototype.drop = function() {
- var el, index, newIndex, oldIndex, reply;
- el = $('.drag', this.parentNode);
- index = function(el) {
- return Array.prototype.slice.call(el.parentNode.children).indexOf(el);
- };
- oldIndex = index(el);
- newIndex = index(this);
- if (oldIndex < newIndex) {
- $.after(this, el);
- } else {
- $.before(this, el);
- }
- reply = QR.replies.splice(oldIndex, 1)[0];
- return QR.replies.splice(newIndex, 0, reply);
- };
-
- _Class.prototype.dragEnd = function() {
- var el;
- $.rmClass(this, 'drag');
- if (el = $('.over', this.parentNode)) {
- return $.rmClass(el, 'over');
- }
- };
-
- _Class.prototype.rm = function() {
- var index, _ref;
- QR.resetFileInput();
- $.rm(this.el);
- index = QR.replies.indexOf(this);
- if (QR.replies.length === 1) {
- new QR.reply().select();
- } else if (this.el.id === 'selected') {
- (QR.replies[index - 1] || QR.replies[index + 1]).select();
- }
- QR.replies.splice(index, 1);
- return (_ref = window.URL || window.webkitURL) != null ? _ref.revokeObjectURL(this.url) : void 0;
- };
-
- return _Class;
-
- })(),
- captcha: {
- init: function() {
- var _this = this;
- if (-1 !== d.cookie.indexOf('pass_enabled=')) {
- return;
- }
- if (!(this.isEnabled = !!$.id('captchaFormPart'))) {
- return;
- }
- if ($.id('recaptcha_challenge_field_holder')) {
- return this.ready();
- } else {
- this.onready = function() {
- return _this.ready();
- };
- return $.on($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready);
- }
- },
- ready: function() {
- var _this = this;
- if (this.challenge = $.id('recaptcha_challenge_field_holder')) {
- $.off($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready);
- delete this.onready;
- } else {
- return;
- }
- $.addClass(QR.el, 'captcha');
- $.after($('.textarea', QR.el), $.el('div', {
- className: 'captchaimg',
- title: 'Reload',
- innerHTML: '
'
- }));
- $.after($('.captchaimg', QR.el), $.el('div', {
- className: 'captchainput',
- innerHTML: ''
- }));
- this.img = $('.captchaimg > img', QR.el);
- this.input = $('.captchainput > input', QR.el);
- $.on(this.img.parentNode, 'click', this.reload);
- $.on(this.input, 'keydown', this.keydown);
- $.on(this.challenge, 'DOMNodeInserted', function() {
- return _this.load();
- });
- $.sync('captchas', function(arr) {
- return _this.count(arr.length);
- });
- this.count($.get('captchas', []).length);
- return this.reload();
- },
- save: function() {
- var captcha, captchas, response;
- if (!(response = this.input.value)) {
- return;
- }
- captchas = $.get('captchas', []);
- while ((captcha = captchas[0]) && captcha.time < Date.now()) {
- captchas.shift();
- }
- captchas.push({
- challenge: this.challenge.firstChild.value,
- response: response,
- time: this.timeout
- });
- $.set('captchas', captchas);
- this.count(captchas.length);
- return this.reload();
- },
- load: function() {
- var challenge;
- this.timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE;
- challenge = this.challenge.firstChild.value;
- this.img.alt = challenge;
- this.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge;
- return this.input.value = null;
- },
- count: function(count) {
- this.input.placeholder = (function() {
- switch (count) {
- case 0:
- return 'Verification (Shift + Enter to cache)';
- case 1:
- return 'Verification (1 cached captcha)';
- default:
- return "Verification (" + count + " cached captchas)";
- }
- })();
- return this.input.alt = count;
- },
- reload: function(focus) {
- $.unsafeWindow.Recaptcha.reload('t');
- if (focus) {
- return QR.captcha.input.focus();
- }
- },
- keydown: function(e) {
- var c;
- c = QR.captcha;
- if (e.keyCode === 8 && !c.input.value) {
- c.reload();
- } else if (e.keyCode === 13 && e.shiftKey) {
- c.save();
- } else {
- return;
- }
- return e.preventDefault();
- }
- },
- dialog: function() {
- var fileInput, key, mimeTypes, name, span, spoiler, ta, thread, threads, _i, _len, _ref, _ref1;
- QR.el = UI.dialog('qr', 'top:0;right:0;', "
\n");
- mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace(/\w+/g, function(type) {
- switch (type) {
- case 'jpg':
- return 'image/jpeg';
- case 'pdf':
- return 'application/pdf';
- case 'swf':
- return 'application/x-shockwave-flash';
- default:
- return "image/" + type;
- }
- });
- QR.mimeTypes = mimeTypes.split(', ');
- QR.mimeTypes.push('');
- fileInput = $('input[type=file]', QR.el);
- fileInput.max = $('input[name=MAX_FILE_SIZE]').value;
- if ($.engine !== 'presto') {
- fileInput.accept = mimeTypes;
- }
- QR.spoiler = !!$('input[name=spoiler]');
- spoiler = $('#spoilerLabel', QR.el);
- spoiler.hidden = !QR.spoiler;
- QR.charaCounter = $('#charCount', QR.el);
- ta = $('textarea', QR.el);
- span = $('.move > span', QR.el);
- if (g.BOARD.ID === 'f') {
- if (g.VIEW === 'index') {
- QR.threadSelector = $('select[name=filetag]').cloneNode(true);
- }
- } else {
- QR.threadSelector = $.el('select', {
- title: 'Create a new thread / Reply to a thread'
- });
- threads = '';
- _ref = g.BOARD.threads;
- for (key in _ref) {
- thread = _ref[key];
- threads += "";
- }
- QR.threadSelector.innerHTML = threads;
- if (g.VIEW === 'thread') {
- QR.threadSelector.value = g.THREAD;
- }
- }
- if (QR.threadSelector) {
- $.prepend(span, QR.threadSelector);
- }
- $.on(span, 'mousedown', function(e) {
- return e.stopPropagation();
- });
- $.on($('#autohide', QR.el), 'change', QR.toggleHide);
- $.on($('.close', QR.el), 'click', QR.close);
- $.on($('#dump', QR.el), 'click', function() {
- return QR.el.classList.toggle('dump');
- });
- $.on($('#addReply', QR.el), 'click', function() {
- return new QR.reply().select();
- });
- $.on($('form', QR.el), 'submit', QR.submit);
- $.on(ta, 'input', function() {
- return QR.selected.el.lastChild.textContent = this.value;
- });
- $.on(ta, 'input', QR.characterCount);
- $.on(fileInput, 'change', QR.fileInput);
- $.on(fileInput, 'click', function(e) {
- if (e.shiftKey) {
- return QR.selected.rmFile() || e.preventDefault();
- }
- });
- $.on(spoiler.firstChild, 'change', function() {
- return $('input', QR.selected.el).click();
- });
- new QR.reply().select();
- _ref1 = ['name', 'email', 'sub', 'com'];
- for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
- name = _ref1[_i];
- $.on($("[name=" + name + "]", QR.el), 'input', function() {
- var _ref2;
- QR.selected[this.name] = this.value;
- if (QR.cooldown.auto && QR.selected === QR.replies[0] && (0 < (_ref2 = QR.cooldown.seconds) && _ref2 <= 5)) {
- return QR.cooldown.auto = false;
- }
- });
- }
- QR.status.input = $('input[type=submit]', QR.el);
- QR.status();
- QR.cooldown.init();
- QR.captcha.init();
- $.add(d.body, QR.el);
- return $.event(new CustomEvent('QRDialogCreation', null, QR.el));
- },
- submit: function(e) {
- var callbacks, captcha, captchas, challenge, err, filetag, m, opts, post, reply, response, textOnly, threadID, _ref;
- if (e != null) {
- e.preventDefault();
- }
- if (QR.cooldown.seconds) {
- QR.cooldown.auto = !QR.cooldown.auto;
- QR.status();
- return;
- }
- QR.abort();
- reply = QR.replies[0];
- if (g.BOARD.ID === 'f' && g.VIEW === 'index') {
- filetag = QR.threadSelector.value;
- threadID = 'new';
- } else {
- threadID = QR.threadSelector.value;
- }
- if (threadID === 'new') {
- threadID = null;
- if (((_ref = g.BOARD.ID) === 'vg' || _ref === 'q') && !reply.sub) {
- err = 'New threads require a subject.';
- } else if (!(reply.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) {
- err = 'No file selected.';
- } else if (g.BOARD.ID === 'f' && filetag === '9999') {
- err = 'Invalid tag specified.';
- }
- } else if (!(reply.com || reply.file)) {
- err = 'No file selected.';
- }
- if (QR.captcha.isEnabled && !err) {
- captchas = $.get('captchas', []);
- while ((captcha = captchas[0]) && captcha.time < Date.now()) {
- captchas.shift();
- }
- if (captcha = captchas.shift()) {
- challenge = captcha.challenge;
- response = captcha.response;
- } else {
- challenge = QR.captcha.img.alt;
- if (response = QR.captcha.input.value) {
- QR.captcha.reload();
- }
- }
- $.set('captchas', captchas);
- QR.captcha.count(captchas.length);
- if (!response) {
- err = 'No valid captcha.';
- } else {
- response = response.trim();
- if (!/\s/.test(response)) {
- response = "" + response + " " + response;
- }
- }
- }
- if (err) {
- QR.cooldown.auto = false;
- QR.status();
- QR.error(err);
- return;
- }
- QR.cleanNotification();
- QR.cooldown.auto = QR.replies.length > 1;
- if (Conf['Auto Hide QR'] && !QR.cooldown.auto) {
- QR.hide();
- }
- if (!QR.cooldown.auto && $.x('ancestor::div[@id="qr"]', d.activeElement)) {
- d.activeElement.blur();
- }
- QR.status({
- progress: '...'
- });
- post = {
- resto: threadID,
- name: reply.name,
- email: reply.email,
- sub: reply.sub,
- com: reply.com,
- upfile: reply.file,
- filetag: filetag,
- spoiler: reply.spoiler,
- textonly: textOnly,
- mode: 'regist',
- pwd: (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value,
- recaptcha_challenge_field: challenge,
- recaptcha_response_field: response
- };
- callbacks = {
- onload: function() {
- return QR.response(this.response);
- },
- onerror: function() {
- QR.cooldown.auto = false;
- QR.status();
- return QR.error($.el('a', {
- href: '//www.4chan.org/banned',
- target: '_blank',
- textContent: 'Connection error, or you are banned.'
- }));
- }
- };
- opts = {
- form: $.formData(post),
- upCallbacks: {
- onload: function() {
- return QR.status({
- progress: '...'
- });
- },
- onprogress: function(e) {
- return QR.status({
- progress: "" + (Math.round(e.loaded / e.total * 100)) + "%"
- });
- }
- }
- };
- return QR.ajax = $.ajax($.id('postForm').parentNode.action, callbacks, opts);
- },
- response: function(html) {
- var ban, board, err, h1, persona, postID, reply, threadID, tmpDoc, _, _ref, _ref1;
- tmpDoc = d.implementation.createHTMLDocument('');
- tmpDoc.documentElement.innerHTML = html;
- if (ban = $('.banType', tmpDoc)) {
- board = $('.board', tmpDoc).innerHTML;
- err = $.el('span', {
- innerHTML: ban.textContent.toLowerCase() === 'banned' ? ("You are banned on " + board + "! ;_;
") + "Click here to see the reason." : ("You were issued a warning on " + board + " as " + ($('.nameBlock', tmpDoc).innerHTML) + ".
") + ("Reason: " + ($('.reason', tmpDoc).innerHTML))
- });
- } else if (err = tmpDoc.getElementById('errmsg')) {
- if ((_ref = $('a', err)) != null) {
- _ref.target = '_blank';
- }
- } else if (tmpDoc.title !== 'Post successful!') {
- err = 'Connection error with sys.4chan.org.';
- }
- if (err) {
- if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') {
- if (/mistyped/i.test(err.textContent)) {
- err = 'Error: You seem to have mistyped the CAPTCHA.';
- }
- QR.cooldown.auto = QR.captcha.isEnabled ? !!$.get('captchas', []).length : err === 'Connection error with sys.4chan.org.' ? true : false;
- QR.cooldown.set({
- delay: 2
- });
- } else {
- QR.cooldown.auto = false;
- }
- QR.status();
- QR.error(err);
- return;
- }
- h1 = $('h1', tmpDoc);
- QR.lastNotification = new Notification('success', h1.textContent, 5);
- reply = QR.replies[0];
- persona = $.get('QR.persona', {});
- persona = {
- name: reply.name,
- email: /^sage$/.test(reply.email) ? persona.email : reply.email,
- sub: Conf['Remember Subject'] ? reply.sub : null
- };
- $.set('QR.persona', persona);
- _ref1 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref1[0], threadID = _ref1[1], postID = _ref1[2];
- $.event(new CustomEvent('QRPostSuccessful', {
- threadID: threadID,
- postID: postID
- }, QR.el));
- QR.cooldown.set({
- post: reply,
- isReply: threadID !== '0'
- });
- QR.cooldown.auto = QR.replies.length > 1;
- if (threadID === '0') {
- $.open("/" + g.BOARD + "/res/" + postID);
- } else if (g.VIEW === 'reply' && !QR.cooldown.auto) {
- $.open("//boards.4chan.org/" + g.BOARD + "/res/" + threadID + "#p" + postID);
- }
- if (Conf['Persistent QR'] || QR.cooldown.auto) {
- reply.rm();
- } else {
- QR.close();
- }
- QR.status();
- return QR.resetFileInput();
- },
- abort: function() {
- var _ref;
- if ((_ref = QR.ajax) != null) {
- _ref.abort();
- }
- delete QR.ajax;
- return QR.status();
- }
- };
-
Menu = {
init: function() {
if (g.VIEW === 'catalog' || !Conf['Menu']) {
@@ -4716,6 +3783,939 @@
})()
};
+ QR = {
+ init: function() {
+ var link;
+ if (g.VIEW === 'catalog' || !Conf['Quick Reply']) {
+ return;
+ }
+ if (Conf['Hide Original Post Form']) {
+ $.addClass(doc, 'hide-original-post-form');
+ }
+ link = $.el('a', {
+ className: 'qr-shortcut',
+ textContent: 'Quick Reply',
+ href: 'javascript:;'
+ });
+ $.on(link, 'click', function() {
+ Header.menu.close();
+ QR.open();
+ if (g.BOARD.ID === 'f') {
+ if (g.VIEW === 'index') {
+ QR.threadSelector.value = '9999';
+ }
+ } else if (g.VIEW === 'thread') {
+ QR.threadSelector.value = g.THREAD;
+ } else {
+ QR.threadSelector.value = 'new';
+ }
+ return $('textarea', QR.el).focus();
+ });
+ $.event('AddMenuEntry', {
+ type: 'header',
+ el: link
+ });
+ $.on(d, 'dragover', QR.dragOver);
+ $.on(d, 'drop', QR.dropFile);
+ $.on(d, 'dragstart dragend', QR.drag);
+ $.on(d, '4chanXInitFinished', function() {
+ if (!Conf['Persistent QR']) {
+ return;
+ }
+ QR.open();
+ if (Conf['Auto Hide QR']) {
+ return QR.hide();
+ }
+ });
+ return Post.prototype.callbacks.push({
+ name: 'Quick Reply',
+ cb: this.node
+ });
+ },
+ node: function() {
+ return $.on($('a[title="Quote this post"]', this.nodes.info), 'click', QR.quote);
+ },
+ open: function() {
+ if (QR.el) {
+ QR.el.hidden = false;
+ QR.unhide();
+ return;
+ }
+ try {
+ return QR.dialog();
+ } catch (err) {
+ delete QR.el;
+ return Main.handleErrors({
+ message: 'Quick Reply dialog creation crashed.',
+ error: err
+ });
+ }
+ },
+ close: function() {
+ var i, spoiler, _i, _len, _ref;
+ QR.el.hidden = true;
+ QR.abort();
+ d.activeElement.blur();
+ $.rmClass(QR.el, 'dump');
+ _ref = QR.replies;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ i = _ref[_i];
+ QR.replies[0].rm();
+ }
+ QR.cooldown.auto = false;
+ QR.status();
+ QR.resetFileInput();
+ if (!Conf['Remember Spoiler'] && (spoiler = $.id('spoiler')).checked) {
+ spoiler.click();
+ }
+ return QR.cleanNotification();
+ },
+ hide: function() {
+ d.activeElement.blur();
+ $.addClass(QR.el, 'autohide');
+ return $.id('autohide').checked = true;
+ },
+ unhide: function() {
+ $.rmClass(QR.el, 'autohide');
+ return $.id('autohide').checked = false;
+ },
+ toggleHide: function() {
+ if (this.checked) {
+ return QR.hide();
+ } else {
+ return QR.unhide();
+ }
+ },
+ error: function(err) {
+ var el;
+ QR.open();
+ if (typeof err === 'string') {
+ el = $.tn(err);
+ } else {
+ el = err;
+ el.removeAttribute('style');
+ }
+ if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
+ $('[autocomplete]', QR.el).focus();
+ }
+ if (d.hidden) {
+ alert(el.textContent);
+ }
+ return QR.lastNotification = new Notification('warning', el);
+ },
+ cleanNotification: function() {
+ var _ref;
+ if ((_ref = QR.lastNotification) != null) {
+ _ref.close();
+ }
+ return delete QR.lastNotification;
+ },
+ status: function(data) {
+ var disabled, input, value;
+ if (data == null) {
+ data = {};
+ }
+ if (!QR.el) {
+ return;
+ }
+ if (g.dead) {
+ value = 404;
+ disabled = true;
+ QR.cooldown.auto = false;
+ }
+ value = data.progress || QR.cooldown.seconds || value;
+ input = QR.status.input;
+ input.value = QR.cooldown.auto ? value ? "Auto " + value : 'Auto' : value || 'Submit';
+ return input.disabled = disabled || false;
+ },
+ cooldown: {
+ init: function() {
+ QR.cooldown.types = {
+ thread: (function() {
+ switch (g.BOARD) {
+ case 'q':
+ return 86400;
+ case 'b':
+ case 'soc':
+ case 'r9k':
+ return 600;
+ default:
+ return 300;
+ }
+ })(),
+ sage: g.BOARD === 'q' ? 600 : 60,
+ file: g.BOARD === 'q' ? 300 : 30,
+ post: g.BOARD === 'q' ? 60 : 30
+ };
+ QR.cooldown.cooldowns = $.get("" + g.BOARD + ".cooldown", {});
+ QR.cooldown.start();
+ return $.sync("" + g.BOARD + ".cooldown", QR.cooldown.sync);
+ },
+ start: function() {
+ if (QR.cooldown.isCounting) {
+ return;
+ }
+ QR.cooldown.isCounting = true;
+ return QR.cooldown.count();
+ },
+ sync: function(cooldowns) {
+ var id;
+ for (id in cooldowns) {
+ QR.cooldown.cooldowns[id] = cooldowns[id];
+ }
+ return QR.cooldown.start();
+ },
+ set: function(data) {
+ var cooldown, hasFile, isReply, isSage, start, type;
+ start = Date.now();
+ if (data.delay) {
+ cooldown = {
+ delay: data.delay
+ };
+ } else {
+ isSage = /sage/i.test(data.post.email);
+ hasFile = !!data.post.file;
+ isReply = data.isReply;
+ type = !isReply ? 'thread' : isSage ? 'sage' : hasFile ? 'file' : 'post';
+ cooldown = {
+ isReply: isReply,
+ isSage: isSage,
+ hasFile: hasFile,
+ timeout: start + QR.cooldown.types[type] * $.SECOND
+ };
+ }
+ QR.cooldown.cooldowns[start] = cooldown;
+ $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns);
+ return QR.cooldown.start();
+ },
+ unset: function(id) {
+ delete QR.cooldown.cooldowns[id];
+ return $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns);
+ },
+ count: function() {
+ var cooldown, cooldowns, elapsed, hasFile, isReply, isSage, now, post, seconds, start, type, types, update, _ref;
+ if (Object.keys(QR.cooldown.cooldowns).length) {
+ setTimeout(QR.cooldown.count, 1000);
+ } else {
+ $["delete"]("" + g.BOARD + ".cooldown");
+ delete QR.cooldown.isCounting;
+ delete QR.cooldown.seconds;
+ QR.status();
+ return;
+ }
+ isReply = g.BOARD.ID === 'f' && g.VIEW === 'thread' ? true : QR.threadSelector.value !== 'new';
+ if (isReply) {
+ post = QR.replies[0];
+ isSage = /sage/i.test(post.email);
+ hasFile = !!post.file;
+ }
+ now = Date.now();
+ seconds = null;
+ _ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns;
+ for (start in cooldowns) {
+ cooldown = cooldowns[start];
+ if ('delay' in cooldown) {
+ if (cooldown.delay) {
+ seconds = Math.max(seconds, cooldown.delay--);
+ } else {
+ seconds = Math.max(seconds, 0);
+ QR.cooldown.unset(start);
+ }
+ continue;
+ }
+ if (isReply === cooldown.isReply) {
+ type = !isReply ? 'thread' : isSage && cooldown.isSage ? 'sage' : hasFile && cooldown.hasFile ? 'file' : 'post';
+ elapsed = Math.floor((now - start) / 1000);
+ if (elapsed >= 0) {
+ seconds = Math.max(seconds, types[type] - elapsed);
+ }
+ }
+ if (!((start <= now && now <= cooldown.timeout))) {
+ QR.cooldown.unset(start);
+ }
+ }
+ update = seconds !== null || !!QR.cooldown.seconds;
+ QR.cooldown.seconds = seconds;
+ if (update) {
+ QR.status();
+ }
+ if (seconds === 0 && QR.cooldown.auto) {
+ return QR.submit();
+ }
+ }
+ },
+ quote: function(e) {
+ var caretPos, post, range, s, sel, selectionRoot, ta, text;
+ if (e != null) {
+ e.preventDefault();
+ }
+ QR.open();
+ ta = $('textarea', QR.el);
+ if (QR.threadSelector && !ta.value && g.BOARD.ID !== 'f') {
+ QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', this).id.slice(1);
+ }
+ post = Get.postFromRoot($.x('ancestor-or-self::div[contains(@class,"postContainer")][1]', this));
+ text = ">>" + post + "\n";
+ sel = d.getSelection();
+ selectionRoot = $.x('ancestor-or-self::div[contains(@class,"postContainer")][1]', sel.anchorNode);
+ if ((s = sel.toString().trim()) && post.nodes.root === selectionRoot) {
+ s = s.replace(/\n/g, '\n>');
+ text += ">" + s + "\n";
+ }
+ caretPos = ta.selectionStart;
+ ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd);
+ range = caretPos + text.length;
+ ta.setSelectionRange(range, range);
+ ta.focus();
+ return ta.dispatchEvent(new Event('input'));
+ },
+ characterCount: function() {
+ var count, counter;
+ counter = QR.charaCounter;
+ count = this.textLength;
+ counter.textContent = count;
+ counter.hidden = count < 1000;
+ return (count > 1500 ? $.addClass : $.rmClass)(counter, 'warning');
+ },
+ drag: function(e) {
+ var toggle;
+ toggle = e.type === 'dragstart' ? $.off : $.on;
+ toggle(d, 'dragover', QR.dragOver);
+ return toggle(d, 'drop', QR.dropFile);
+ },
+ dragOver: function(e) {
+ e.preventDefault();
+ return e.dataTransfer.dropEffect = 'copy';
+ },
+ dropFile: function(e) {
+ if (!e.dataTransfer.files.length) {
+ return;
+ }
+ e.preventDefault();
+ QR.open();
+ QR.fileInput.call(e.dataTransfer);
+ return $.addClass(QR.el, 'dump');
+ },
+ fileInput: function() {
+ var file, _i, _len, _ref;
+ QR.cleanNotification();
+ if (this.files.length === 1) {
+ file = this.files[0];
+ if (file.size > this.max) {
+ QR.error('File too large.');
+ QR.resetFileInput();
+ } else if (-1 === QR.mimeTypes.indexOf(file.type)) {
+ QR.error('Unsupported file type.');
+ QR.resetFileInput();
+ } else {
+ QR.selected.setFile(file);
+ }
+ return;
+ }
+ _ref = this.files;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ file = _ref[_i];
+ if (file.size > this.max) {
+ QR.error("File " + file.name + " is too large.");
+ break;
+ } else if (-1 === QR.mimeTypes.indexOf(file.type)) {
+ QR.error("" + file.name + ": Unsupported file type.");
+ break;
+ }
+ if (!QR.replies[QR.replies.length - 1].file) {
+ QR.replies[QR.replies.length - 1].setFile(file);
+ } else {
+ new QR.reply().setFile(file);
+ }
+ }
+ $.addClass(QR.el, 'dump');
+ return QR.resetFileInput();
+ },
+ resetFileInput: function() {
+ return $('[type=file]', QR.el).value = null;
+ },
+ replies: [],
+ reply: (function() {
+
+ function _Class() {
+ var persona, prev,
+ _this = this;
+ prev = QR.replies[QR.replies.length - 1];
+ persona = $.get('QR.persona', {});
+ this.name = prev ? prev.name : persona.name || null;
+ this.email = prev && !/^sage$/.test(prev.email) ? prev.email : persona.email || null;
+ this.sub = prev && Conf['Remember Subject'] ? prev.sub : Conf['Remember Subject'] ? persona.sub : null;
+ this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false;
+ this.com = null;
+ this.el = $.el('a', {
+ className: 'qrpreview',
+ draggable: true,
+ href: 'javascript:;',
+ innerHTML: '×'
+ });
+ $('input', this.el).checked = this.spoiler;
+ $.on(this.el, 'click', function() {
+ return _this.select();
+ });
+ $.on($('.remove', this.el), 'click', function(e) {
+ e.stopPropagation();
+ return _this.rm();
+ });
+ $.on($('label', this.el), 'click', function(e) {
+ return e.stopPropagation();
+ });
+ $.on($('input', this.el), 'change', function(e) {
+ _this.spoiler = e.target.checked;
+ if (_this.el.id === 'selected') {
+ return $.id('spoiler').checked = _this.spoiler;
+ }
+ });
+ $.before($('#addReply', QR.el), this.el);
+ $.on(this.el, 'dragstart', this.dragStart);
+ $.on(this.el, 'dragenter', this.dragEnter);
+ $.on(this.el, 'dragleave', this.dragLeave);
+ $.on(this.el, 'dragover', this.dragOver);
+ $.on(this.el, 'dragend', this.dragEnd);
+ $.on(this.el, 'drop', this.drop);
+ QR.replies.push(this);
+ }
+
+ _Class.prototype.setFile = function(file) {
+ var fileUrl, img, url,
+ _this = this;
+ this.file = file;
+ this.el.title = "" + file.name + " (" + ($.bytesToString(file.size)) + ")";
+ if (QR.spoiler) {
+ $('label', this.el).hidden = false;
+ }
+ if (!/^image/.test(file.type)) {
+ this.el.style.backgroundImage = null;
+ return;
+ }
+ if (!(url = window.URL || window.webkitURL)) {
+ return;
+ }
+ url.revokeObjectURL(this.url);
+ fileUrl = url.createObjectURL(file);
+ img = $.el('img');
+ $.on(img, 'load', function() {
+ var c, data, i, l, s, ui8a, _i;
+ s = 90 * 3;
+ if (img.height < s || img.width < s) {
+ _this.url = fileUrl;
+ _this.el.style.backgroundImage = "url(" + _this.url + ")";
+ return;
+ }
+ if (img.height <= img.width) {
+ img.width = s / img.height * img.width;
+ img.height = s;
+ } else {
+ img.height = s / img.width * img.height;
+ img.width = s;
+ }
+ c = $.el('canvas');
+ c.height = img.height;
+ c.width = img.width;
+ c.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
+ data = atob(c.toDataURL().split(',')[1]);
+ l = data.length;
+ ui8a = new Uint8Array(l);
+ for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) {
+ ui8a[i] = data.charCodeAt(i);
+ }
+ _this.url = url.createObjectURL(new Blob([ui8a], {
+ type: 'image/png'
+ }));
+ _this.el.style.backgroundImage = "url(" + _this.url + ")";
+ return typeof url.revokeObjectURL === "function" ? url.revokeObjectURL(fileUrl) : void 0;
+ });
+ return img.src = fileUrl;
+ };
+
+ _Class.prototype.rmFile = function() {
+ var _base;
+ QR.resetFileInput();
+ delete this.file;
+ this.el.title = null;
+ this.el.style.backgroundImage = null;
+ if (QR.spoiler) {
+ $('label', this.el).hidden = true;
+ }
+ return typeof (_base = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base.revokeObjectURL(this.url) : void 0;
+ };
+
+ _Class.prototype.select = function() {
+ var data, rectEl, rectList, _i, _len, _ref, _ref1;
+ if ((_ref = QR.selected) != null) {
+ _ref.el.id = null;
+ }
+ QR.selected = this;
+ this.el.id = 'selected';
+ rectEl = this.el.getBoundingClientRect();
+ rectList = this.el.parentNode.getBoundingClientRect();
+ this.el.parentNode.scrollLeft += rectEl.left + rectEl.width / 2 - rectList.left - rectList.width / 2;
+ _ref1 = ['name', 'email', 'sub', 'com'];
+ for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
+ data = _ref1[_i];
+ $("[name=" + data + "]", QR.el).value = this[data];
+ }
+ QR.characterCount.call($('textarea', QR.el));
+ return $('#spoiler', QR.el).checked = this.spoiler;
+ };
+
+ _Class.prototype.dragStart = function() {
+ return $.addClass(this, 'drag');
+ };
+
+ _Class.prototype.dragEnter = function() {
+ return $.addClass(this, 'over');
+ };
+
+ _Class.prototype.dragLeave = function() {
+ return $.rmClass(this, 'over');
+ };
+
+ _Class.prototype.dragOver = function(e) {
+ e.preventDefault();
+ return e.dataTransfer.dropEffect = 'move';
+ };
+
+ _Class.prototype.drop = function() {
+ var el, index, newIndex, oldIndex, reply;
+ el = $('.drag', this.parentNode);
+ index = function(el) {
+ return Array.prototype.slice.call(el.parentNode.children).indexOf(el);
+ };
+ oldIndex = index(el);
+ newIndex = index(this);
+ if (oldIndex < newIndex) {
+ $.after(this, el);
+ } else {
+ $.before(this, el);
+ }
+ reply = QR.replies.splice(oldIndex, 1)[0];
+ return QR.replies.splice(newIndex, 0, reply);
+ };
+
+ _Class.prototype.dragEnd = function() {
+ var el;
+ $.rmClass(this, 'drag');
+ if (el = $('.over', this.parentNode)) {
+ return $.rmClass(el, 'over');
+ }
+ };
+
+ _Class.prototype.rm = function() {
+ var index, _ref;
+ QR.resetFileInput();
+ $.rm(this.el);
+ index = QR.replies.indexOf(this);
+ if (QR.replies.length === 1) {
+ new QR.reply().select();
+ } else if (this.el.id === 'selected') {
+ (QR.replies[index - 1] || QR.replies[index + 1]).select();
+ }
+ QR.replies.splice(index, 1);
+ return (_ref = window.URL || window.webkitURL) != null ? _ref.revokeObjectURL(this.url) : void 0;
+ };
+
+ return _Class;
+
+ })(),
+ captcha: {
+ init: function() {
+ var _this = this;
+ if (-1 !== d.cookie.indexOf('pass_enabled=')) {
+ return;
+ }
+ if (!(this.isEnabled = !!$.id('captchaFormPart'))) {
+ return;
+ }
+ if ($.id('recaptcha_challenge_field_holder')) {
+ return this.ready();
+ } else {
+ this.onready = function() {
+ return _this.ready();
+ };
+ return $.on($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready);
+ }
+ },
+ ready: function() {
+ var _this = this;
+ if (this.challenge = $.id('recaptcha_challenge_field_holder')) {
+ $.off($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready);
+ delete this.onready;
+ } else {
+ return;
+ }
+ $.addClass(QR.el, 'captcha');
+ $.after($('.textarea', QR.el), $.el('div', {
+ className: 'captchaimg',
+ title: 'Reload',
+ innerHTML: '
'
+ }));
+ $.after($('.captchaimg', QR.el), $.el('div', {
+ className: 'captchainput',
+ innerHTML: ''
+ }));
+ this.img = $('.captchaimg > img', QR.el);
+ this.input = $('.captchainput > input', QR.el);
+ $.on(this.img.parentNode, 'click', this.reload);
+ $.on(this.input, 'keydown', this.keydown);
+ $.on(this.challenge, 'DOMNodeInserted', function() {
+ return _this.load();
+ });
+ $.sync('captchas', function(arr) {
+ return _this.count(arr.length);
+ });
+ this.count($.get('captchas', []).length);
+ return this.reload();
+ },
+ save: function() {
+ var captcha, captchas, response;
+ if (!(response = this.input.value)) {
+ return;
+ }
+ captchas = $.get('captchas', []);
+ while ((captcha = captchas[0]) && captcha.time < Date.now()) {
+ captchas.shift();
+ }
+ captchas.push({
+ challenge: this.challenge.firstChild.value,
+ response: response,
+ time: this.timeout
+ });
+ $.set('captchas', captchas);
+ this.count(captchas.length);
+ return this.reload();
+ },
+ load: function() {
+ var challenge;
+ this.timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE;
+ challenge = this.challenge.firstChild.value;
+ this.img.alt = challenge;
+ this.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge;
+ return this.input.value = null;
+ },
+ count: function(count) {
+ this.input.placeholder = (function() {
+ switch (count) {
+ case 0:
+ return 'Verification (Shift + Enter to cache)';
+ case 1:
+ return 'Verification (1 cached captcha)';
+ default:
+ return "Verification (" + count + " cached captchas)";
+ }
+ })();
+ return this.input.alt = count;
+ },
+ reload: function(focus) {
+ $.unsafeWindow.Recaptcha.reload('t');
+ if (focus) {
+ return QR.captcha.input.focus();
+ }
+ },
+ keydown: function(e) {
+ var c;
+ c = QR.captcha;
+ if (e.keyCode === 8 && !c.input.value) {
+ c.reload();
+ } else if (e.keyCode === 13 && e.shiftKey) {
+ c.save();
+ } else {
+ return;
+ }
+ return e.preventDefault();
+ }
+ },
+ dialog: function() {
+ var fileInput, key, mimeTypes, name, span, spoiler, ta, thread, threads, _i, _len, _ref, _ref1;
+ QR.el = UI.dialog('qr', 'top:0;right:0;', "\n");
+ mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace(/\w+/g, function(type) {
+ switch (type) {
+ case 'jpg':
+ return 'image/jpeg';
+ case 'pdf':
+ return 'application/pdf';
+ case 'swf':
+ return 'application/x-shockwave-flash';
+ default:
+ return "image/" + type;
+ }
+ });
+ QR.mimeTypes = mimeTypes.split(', ');
+ QR.mimeTypes.push('');
+ fileInput = $('input[type=file]', QR.el);
+ fileInput.max = $('input[name=MAX_FILE_SIZE]').value;
+ if ($.engine !== 'presto') {
+ fileInput.accept = mimeTypes;
+ }
+ QR.spoiler = !!$('input[name=spoiler]');
+ spoiler = $('#spoilerLabel', QR.el);
+ spoiler.hidden = !QR.spoiler;
+ QR.charaCounter = $('#charCount', QR.el);
+ ta = $('textarea', QR.el);
+ span = $('.move > span', QR.el);
+ if (g.BOARD.ID === 'f') {
+ if (g.VIEW === 'index') {
+ QR.threadSelector = $('select[name=filetag]').cloneNode(true);
+ }
+ } else {
+ QR.threadSelector = $.el('select', {
+ title: 'Create a new thread / Reply to a thread'
+ });
+ threads = '';
+ _ref = g.BOARD.threads;
+ for (key in _ref) {
+ thread = _ref[key];
+ threads += "";
+ }
+ QR.threadSelector.innerHTML = threads;
+ if (g.VIEW === 'thread') {
+ QR.threadSelector.value = g.THREAD;
+ }
+ }
+ if (QR.threadSelector) {
+ $.prepend(span, QR.threadSelector);
+ }
+ $.on(span, 'mousedown', function(e) {
+ return e.stopPropagation();
+ });
+ $.on($('#autohide', QR.el), 'change', QR.toggleHide);
+ $.on($('.close', QR.el), 'click', QR.close);
+ $.on($('#dump', QR.el), 'click', function() {
+ return QR.el.classList.toggle('dump');
+ });
+ $.on($('#addReply', QR.el), 'click', function() {
+ return new QR.reply().select();
+ });
+ $.on($('form', QR.el), 'submit', QR.submit);
+ $.on(ta, 'input', function() {
+ return QR.selected.el.lastChild.textContent = this.value;
+ });
+ $.on(ta, 'input', QR.characterCount);
+ $.on(fileInput, 'change', QR.fileInput);
+ $.on(fileInput, 'click', function(e) {
+ if (e.shiftKey) {
+ return QR.selected.rmFile() || e.preventDefault();
+ }
+ });
+ $.on(spoiler.firstChild, 'change', function() {
+ return $('input', QR.selected.el).click();
+ });
+ new QR.reply().select();
+ _ref1 = ['name', 'email', 'sub', 'com'];
+ for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
+ name = _ref1[_i];
+ $.on($("[name=" + name + "]", QR.el), 'input', function() {
+ var _ref2;
+ QR.selected[this.name] = this.value;
+ if (QR.cooldown.auto && QR.selected === QR.replies[0] && (0 < (_ref2 = QR.cooldown.seconds) && _ref2 <= 5)) {
+ return QR.cooldown.auto = false;
+ }
+ });
+ }
+ QR.status.input = $('input[type=submit]', QR.el);
+ QR.status();
+ QR.cooldown.init();
+ QR.captcha.init();
+ $.add(d.body, QR.el);
+ return $.event(new CustomEvent('QRDialogCreation', null, QR.el));
+ },
+ submit: function(e) {
+ var callbacks, captcha, captchas, challenge, err, filetag, m, opts, post, reply, response, textOnly, threadID, _ref;
+ if (e != null) {
+ e.preventDefault();
+ }
+ if (QR.cooldown.seconds) {
+ QR.cooldown.auto = !QR.cooldown.auto;
+ QR.status();
+ return;
+ }
+ QR.abort();
+ reply = QR.replies[0];
+ if (g.BOARD.ID === 'f' && g.VIEW === 'index') {
+ filetag = QR.threadSelector.value;
+ threadID = 'new';
+ } else {
+ threadID = QR.threadSelector.value;
+ }
+ if (threadID === 'new') {
+ threadID = null;
+ if (((_ref = g.BOARD.ID) === 'vg' || _ref === 'q') && !reply.sub) {
+ err = 'New threads require a subject.';
+ } else if (!(reply.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) {
+ err = 'No file selected.';
+ } else if (g.BOARD.ID === 'f' && filetag === '9999') {
+ err = 'Invalid tag specified.';
+ }
+ } else if (!(reply.com || reply.file)) {
+ err = 'No file selected.';
+ }
+ if (QR.captcha.isEnabled && !err) {
+ captchas = $.get('captchas', []);
+ while ((captcha = captchas[0]) && captcha.time < Date.now()) {
+ captchas.shift();
+ }
+ if (captcha = captchas.shift()) {
+ challenge = captcha.challenge;
+ response = captcha.response;
+ } else {
+ challenge = QR.captcha.img.alt;
+ if (response = QR.captcha.input.value) {
+ QR.captcha.reload();
+ }
+ }
+ $.set('captchas', captchas);
+ QR.captcha.count(captchas.length);
+ if (!response) {
+ err = 'No valid captcha.';
+ } else {
+ response = response.trim();
+ if (!/\s/.test(response)) {
+ response = "" + response + " " + response;
+ }
+ }
+ }
+ if (err) {
+ QR.cooldown.auto = false;
+ QR.status();
+ QR.error(err);
+ return;
+ }
+ QR.cleanNotification();
+ QR.cooldown.auto = QR.replies.length > 1;
+ if (Conf['Auto Hide QR'] && !QR.cooldown.auto) {
+ QR.hide();
+ }
+ if (!QR.cooldown.auto && $.x('ancestor::div[@id="qr"]', d.activeElement)) {
+ d.activeElement.blur();
+ }
+ QR.status({
+ progress: '...'
+ });
+ post = {
+ resto: threadID,
+ name: reply.name,
+ email: reply.email,
+ sub: reply.sub,
+ com: reply.com,
+ upfile: reply.file,
+ filetag: filetag,
+ spoiler: reply.spoiler,
+ textonly: textOnly,
+ mode: 'regist',
+ pwd: (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value,
+ recaptcha_challenge_field: challenge,
+ recaptcha_response_field: response
+ };
+ callbacks = {
+ onload: function() {
+ return QR.response(this.response);
+ },
+ onerror: function() {
+ QR.cooldown.auto = false;
+ QR.status();
+ return QR.error($.el('a', {
+ href: '//www.4chan.org/banned',
+ target: '_blank',
+ textContent: 'Connection error, or you are banned.'
+ }));
+ }
+ };
+ opts = {
+ form: $.formData(post),
+ upCallbacks: {
+ onload: function() {
+ return QR.status({
+ progress: '...'
+ });
+ },
+ onprogress: function(e) {
+ return QR.status({
+ progress: "" + (Math.round(e.loaded / e.total * 100)) + "%"
+ });
+ }
+ }
+ };
+ return QR.ajax = $.ajax($.id('postForm').parentNode.action, callbacks, opts);
+ },
+ response: function(html) {
+ var ban, board, err, h1, persona, postID, reply, threadID, tmpDoc, _, _ref, _ref1;
+ tmpDoc = d.implementation.createHTMLDocument('');
+ tmpDoc.documentElement.innerHTML = html;
+ if (ban = $('.banType', tmpDoc)) {
+ board = $('.board', tmpDoc).innerHTML;
+ err = $.el('span', {
+ innerHTML: ban.textContent.toLowerCase() === 'banned' ? ("You are banned on " + board + "! ;_;
") + "Click here to see the reason." : ("You were issued a warning on " + board + " as " + ($('.nameBlock', tmpDoc).innerHTML) + ".
") + ("Reason: " + ($('.reason', tmpDoc).innerHTML))
+ });
+ } else if (err = tmpDoc.getElementById('errmsg')) {
+ if ((_ref = $('a', err)) != null) {
+ _ref.target = '_blank';
+ }
+ } else if (tmpDoc.title !== 'Post successful!') {
+ err = 'Connection error with sys.4chan.org.';
+ }
+ if (err) {
+ if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') {
+ if (/mistyped/i.test(err.textContent)) {
+ err = 'Error: You seem to have mistyped the CAPTCHA.';
+ }
+ QR.cooldown.auto = QR.captcha.isEnabled ? !!$.get('captchas', []).length : err === 'Connection error with sys.4chan.org.' ? true : false;
+ QR.cooldown.set({
+ delay: 2
+ });
+ } else {
+ QR.cooldown.auto = false;
+ }
+ QR.status();
+ QR.error(err);
+ return;
+ }
+ h1 = $('h1', tmpDoc);
+ QR.lastNotification = new Notification('success', h1.textContent, 5);
+ reply = QR.replies[0];
+ persona = $.get('QR.persona', {});
+ persona = {
+ name: reply.name,
+ email: /^sage$/.test(reply.email) ? persona.email : reply.email,
+ sub: Conf['Remember Subject'] ? reply.sub : null
+ };
+ $.set('QR.persona', persona);
+ _ref1 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref1[0], threadID = _ref1[1], postID = _ref1[2];
+ $.event(new CustomEvent('QRPostSuccessful', {
+ threadID: threadID,
+ postID: postID
+ }, QR.el));
+ QR.cooldown.set({
+ post: reply,
+ isReply: threadID !== '0'
+ });
+ QR.cooldown.auto = QR.replies.length > 1;
+ if (threadID === '0') {
+ $.open("/" + g.BOARD + "/res/" + postID);
+ } else if (g.VIEW === 'reply' && !QR.cooldown.auto) {
+ $.open("//boards.4chan.org/" + g.BOARD + "/res/" + threadID + "#p" + postID);
+ }
+ if (Conf['Persistent QR'] || QR.cooldown.auto) {
+ reply.rm();
+ } else {
+ QR.close();
+ }
+ QR.status();
+ return QR.resetFileInput();
+ },
+ abort: function() {
+ var _ref;
+ if ((_ref = QR.ajax) != null) {
+ _ref.abort();
+ }
+ delete QR.ajax;
+ return QR.status();
+ }
+ };
+
Board = (function() {
Board.prototype.toString = function() {
diff --git a/grunt.js b/grunt.js
index c5c392104..03635bef3 100644
--- a/grunt.js
+++ b/grunt.js
@@ -20,6 +20,7 @@ module.exports = function(grunt) {
'',
'',
'',
+ '',
''
],
dest: 'tmp/script.coffee'
diff --git a/src/features.coffee b/src/features.coffee
index 2ea742d45..c217d3a02 100644
--- a/src/features.coffee
+++ b/src/features.coffee
@@ -760,803 +760,6 @@ Recursive =
break
return
-QR =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Quick Reply']
-
- if Conf['Hide Original Post Form']
- $.addClass doc, 'hide-original-post-form'
-
- link = $.el 'a',
- className: 'qr-shortcut'
- textContent: 'Quick Reply'
- href: 'javascript:;'
- $.on link, 'click', ->
- Header.menu.close()
- QR.open()
- if g.BOARD.ID is 'f'
- if g.VIEW is 'index'
- QR.threadSelector.value = '9999'
- else if g.VIEW is 'thread'
- QR.threadSelector.value = g.THREAD
- else
- QR.threadSelector.value = 'new'
- $('textarea', QR.el).focus()
- $.event 'AddMenuEntry',
- type: 'header'
- el: link
-
- $.on d, 'dragover', QR.dragOver
- $.on d, 'drop', QR.dropFile
- $.on d, 'dragstart dragend', QR.drag
- $.on d, '4chanXInitFinished', ->
- return unless Conf['Persistent QR']
- QR.open()
- QR.hide() if Conf['Auto Hide QR']
-
- Post::callbacks.push
- name: 'Quick Reply'
- cb: @node
-
- node: ->
- $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
-
- open: ->
- if QR.el
- QR.el.hidden = false
- QR.unhide()
- return
- try
- QR.dialog()
- catch err
- delete QR.el
- Main.handleErrors
- message: 'Quick Reply dialog creation crashed.'
- error: err
- close: ->
- QR.el.hidden = true
- QR.abort()
- d.activeElement.blur()
- $.rmClass QR.el, 'dump'
- for i in QR.replies
- QR.replies[0].rm()
- QR.cooldown.auto = false
- QR.status()
- QR.resetFileInput()
- if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked
- spoiler.click()
- QR.cleanNotification()
- hide: ->
- d.activeElement.blur()
- $.addClass QR.el, 'autohide'
- $.id('autohide').checked = true
- unhide: ->
- $.rmClass QR.el, 'autohide'
- $.id('autohide').checked = false
- toggleHide: ->
- if @checked
- QR.hide()
- else
- QR.unhide()
-
- error: (err) ->
- QR.open()
- if typeof err is 'string'
- el = $.tn err
- else
- el = err
- el.removeAttribute 'style'
- if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
- # Focus the captcha input on captcha error.
- $('[autocomplete]', QR.el).focus()
- alert el.textContent if d.hidden
- QR.lastNotification = new Notification 'warning', el
- cleanNotification: ->
- QR.lastNotification?.close()
- delete QR.lastNotification
-
- status: (data={}) ->
- return unless QR.el
- if g.dead # XXX
- value = 404
- disabled = true
- QR.cooldown.auto = false
- value = data.progress or QR.cooldown.seconds or value
- {input} = QR.status
- input.value =
- if QR.cooldown.auto
- if value then "Auto #{value}" else 'Auto'
- else
- value or 'Submit'
- input.disabled = disabled or false
-
- cooldown:
- init: ->
- QR.cooldown.types =
- thread: switch g.BOARD
- when 'q' then 86400
- when 'b', 'soc', 'r9k' then 600
- else 300
- sage: if g.BOARD is 'q' then 600 else 60
- file: if g.BOARD is 'q' then 300 else 30
- post: if g.BOARD is 'q' then 60 else 30
- QR.cooldown.cooldowns = $.get "#{g.BOARD}.cooldown", {}
- QR.cooldown.start()
- $.sync "#{g.BOARD}.cooldown", QR.cooldown.sync
- start: ->
- return if QR.cooldown.isCounting
- QR.cooldown.isCounting = true
- QR.cooldown.count()
- sync: (cooldowns) ->
- # Add each cooldowns, don't overwrite everything in case we
- # still need to prune one in the current tab to auto-post.
- for id of cooldowns
- QR.cooldown.cooldowns[id] = cooldowns[id]
- QR.cooldown.start()
- set: (data) ->
- start = Date.now()
- if data.delay
- cooldown = delay: data.delay
- else
- isSage = /sage/i.test data.post.email
- hasFile = !!data.post.file
- isReply = data.isReply
- type =
- unless isReply
- 'thread'
- else if isSage
- 'sage'
- else if hasFile
- 'file'
- else
- 'post'
- cooldown =
- isReply: isReply
- isSage: isSage
- hasFile: hasFile
- timeout: start + QR.cooldown.types[type] * $.SECOND
- QR.cooldown.cooldowns[start] = cooldown
- $.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns
- QR.cooldown.start()
- unset: (id) ->
- delete QR.cooldown.cooldowns[id]
- $.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns
- count: ->
- if Object.keys(QR.cooldown.cooldowns).length
- setTimeout QR.cooldown.count, 1000
- else
- $.delete "#{g.BOARD}.cooldown"
- delete QR.cooldown.isCounting
- delete QR.cooldown.seconds
- QR.status()
- return
-
- isReply =
- if g.BOARD.ID is 'f' and g.VIEW is 'thread'
- true
- else
- QR.threadSelector.value isnt 'new'
- if isReply
- post = QR.replies[0]
- isSage = /sage/i.test post.email
- hasFile = !!post.file
- now = Date.now()
- seconds = null
- {types, cooldowns} = QR.cooldown
-
- for start, cooldown of cooldowns
- if 'delay' of cooldown
- if cooldown.delay
- seconds = Math.max seconds, cooldown.delay--
- else
- seconds = Math.max seconds, 0
- QR.cooldown.unset start
- continue
-
- if isReply is cooldown.isReply
- # Only cooldowns relevant to this post can set the seconds value.
- # Unset outdated cooldowns that can no longer impact us.
- type =
- unless isReply
- 'thread'
- else if isSage and cooldown.isSage
- 'sage'
- else if hasFile and cooldown.hasFile
- 'file'
- else
- 'post'
- elapsed = Math.floor (now - start) / 1000
- if elapsed >= 0 # clock changed since then?
- seconds = Math.max seconds, types[type] - elapsed
- unless start <= now <= cooldown.timeout
- QR.cooldown.unset start
-
- # Update the status when we change posting type.
- # Don't get stuck at some random number.
- # Don't interfere with progress status updates.
- update = seconds isnt null or !!QR.cooldown.seconds
- QR.cooldown.seconds = seconds
- QR.status() if update
- QR.submit() if seconds is 0 and QR.cooldown.auto
-
- quote: (e) ->
- e?.preventDefault()
- QR.open()
- ta = $ 'textarea', QR.el
- if QR.threadSelector and !ta.value and g.BOARD.ID isnt 'f'
- QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..]
- # Make sure we get the correct number, even with XXX censors
- post = Get.postFromRoot $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', @
- text = ">>#{post}\n"
-
- sel = d.getSelection()
- selectionRoot = $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', sel.anchorNode
- if (s = sel.toString().trim()) and post.nodes.root is selectionRoot
- # XXX Opera doesn't retain `\n`s?
- s = s.replace /\n/g, '\n>'
- text += ">#{s}\n"
-
- caretPos = ta.selectionStart
- # Replace selection for text.
- ta.value = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..]
- # Move the caret to the end of the new quote.
- range = caretPos + text.length
- ta.setSelectionRange range, range
- ta.focus()
-
- # Fire the 'input' event
- ta.dispatchEvent new Event 'input'
-
- characterCount: ->
- counter = QR.charaCounter
- count = @textLength
- counter.textContent = count
- counter.hidden = count < 1000
- (if count > 1500 then $.addClass else $.rmClass) counter, 'warning'
-
- drag: (e) ->
- # Let it drag anything from the page.
- toggle = if e.type is 'dragstart' then $.off else $.on
- toggle d, 'dragover', QR.dragOver
- toggle d, 'drop', QR.dropFile
- dragOver: (e) ->
- e.preventDefault()
- e.dataTransfer.dropEffect = 'copy' # cursor feedback
- dropFile: (e) ->
- # Let it only handle files from the desktop.
- return unless e.dataTransfer.files.length
- e.preventDefault()
- QR.open()
- QR.fileInput.call e.dataTransfer
- $.addClass QR.el, 'dump'
- fileInput: ->
- QR.cleanNotification()
- # Set or change current reply's file.
- if @files.length is 1
- file = @files[0]
- if file.size > @max
- QR.error 'File too large.'
- QR.resetFileInput()
- else if -1 is QR.mimeTypes.indexOf file.type
- QR.error 'Unsupported file type.'
- QR.resetFileInput()
- else
- QR.selected.setFile file
- return
- # Create new replies with these files.
- for file in @files
- if file.size > @max
- QR.error "File #{file.name} is too large."
- break
- else if -1 is QR.mimeTypes.indexOf file.type
- QR.error "#{file.name}: Unsupported file type."
- break
- unless QR.replies[QR.replies.length - 1].file
- # set last reply's file
- QR.replies[QR.replies.length - 1].setFile file
- else
- new QR.reply().setFile file
- $.addClass QR.el, 'dump'
- QR.resetFileInput() # reset input
- resetFileInput: ->
- $('[type=file]', QR.el).value = null
-
- replies: []
- reply: class
- constructor: ->
- # set values, or null, to avoid 'undefined' values in inputs
- prev = QR.replies[QR.replies.length-1]
- persona = $.get 'QR.persona', {}
- @name = if prev then prev.name else persona.name or null
- @email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null
- @sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null
- @spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false
- @com = null
-
- @el = $.el 'a',
- className: 'qrpreview'
- draggable: true
- href: 'javascript:;'
- innerHTML: '×'
- $('input', @el).checked = @spoiler
- $.on @el, 'click', => @select()
- $.on $('.remove', @el), 'click', (e) =>
- e.stopPropagation()
- @rm()
- $.on $('label', @el), 'click', (e) => e.stopPropagation()
- $.on $('input', @el), 'change', (e) =>
- @spoiler = e.target.checked
- $.id('spoiler').checked = @spoiler if @el.id is 'selected'
- $.before $('#addReply', QR.el), @el
-
- $.on @el, 'dragstart', @dragStart
- $.on @el, 'dragenter', @dragEnter
- $.on @el, 'dragleave', @dragLeave
- $.on @el, 'dragover', @dragOver
- $.on @el, 'dragend', @dragEnd
- $.on @el, 'drop', @drop
-
- QR.replies.push @
- setFile: (@file) ->
- @el.title = "#{file.name} (#{$.bytesToString file.size})"
- $('label', @el).hidden = false if QR.spoiler
- unless /^image/.test file.type
- @el.style.backgroundImage = null
- return
- # XXX Opera does not support window.URL
- return unless url = window.URL or window.webkitURL
- url.revokeObjectURL @url
-
- # Create a redimensioned thumbnail.
- fileUrl = url.createObjectURL file
- img = $.el 'img'
-
- $.on img, 'load', =>
- # Generate thumbnails only if they're really big.
- # Resized pictures through canvases look like ass,
- # so we generate thumbnails `s` times bigger then expected
- # to avoid crappy resized quality.
- s = 90*3
- if img.height < s or img.width < s
- @url = fileUrl
- @el.style.backgroundImage = "url(#{@url})"
- return
- if img.height <= img.width
- img.width = s / img.height * img.width
- img.height = s
- else
- img.height = s / img.width * img.height
- img.width = s
- c = $.el 'canvas'
- c.height = img.height
- c.width = img.width
- c.getContext('2d').drawImage img, 0, 0, img.width, img.height
- # Support for toBlob fucking when?
- data = atob c.toDataURL().split(',')[1]
-
- # DataUrl to Binary code from Aeosynth's 4chan X repo
- l = data.length
- ui8a = new Uint8Array l
- for i in [0...l]
- ui8a[i] = data.charCodeAt i
-
- @url = url.createObjectURL new Blob [ui8a], type: 'image/png'
- @el.style.backgroundImage = "url(#{@url})"
- url.revokeObjectURL? fileUrl
-
- img.src = fileUrl
- rmFile: ->
- QR.resetFileInput()
- delete @file
- @el.title = null
- @el.style.backgroundImage = null
- $('label', @el).hidden = true if QR.spoiler
- (window.URL or window.webkitURL).revokeObjectURL? @url
- select: ->
- QR.selected?.el.id = null
- QR.selected = @
- @el.id = 'selected'
- # Scroll the list to center the focused reply.
- rectEl = @el.getBoundingClientRect()
- rectList = @el.parentNode.getBoundingClientRect()
- @el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2
- # Load this reply's values.
- for data in ['name', 'email', 'sub', 'com']
- $("[name=#{data}]", QR.el).value = @[data]
- QR.characterCount.call $ 'textarea', QR.el
- $('#spoiler', QR.el).checked = @spoiler
- dragStart: ->
- $.addClass @, 'drag'
- dragEnter: ->
- $.addClass @, 'over'
- dragLeave: ->
- $.rmClass @, 'over'
- dragOver: (e) ->
- e.preventDefault()
- e.dataTransfer.dropEffect = 'move'
- drop: ->
- el = $ '.drag', @parentNode
- index = (el) -> Array::slice.call(el.parentNode.children).indexOf el
- oldIndex = index el
- newIndex = index @
- if oldIndex < newIndex
- $.after @, el
- else
- $.before @, el
- reply = QR.replies.splice(oldIndex, 1)[0]
- QR.replies.splice newIndex, 0, reply
- dragEnd: ->
- $.rmClass @, 'drag'
- if el = $ '.over', @parentNode
- $.rmClass el, 'over'
- rm: ->
- QR.resetFileInput()
- $.rm @el
- index = QR.replies.indexOf @
- if QR.replies.length is 1
- new QR.reply().select()
- else if @el.id is 'selected'
- (QR.replies[index-1] or QR.replies[index+1]).select()
- QR.replies.splice index, 1
- (window.URL or window.webkitURL)?.revokeObjectURL @url
-
- captcha:
- init: ->
- return if -1 isnt d.cookie.indexOf 'pass_enabled='
- return unless @isEnabled = !!$.id 'captchaFormPart'
- if $.id 'recaptcha_challenge_field_holder'
- @ready()
- else
- @onready = => @ready()
- $.on $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready
- ready: ->
- if @challenge = $.id 'recaptcha_challenge_field_holder'
- $.off $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready
- delete @onready
- else
- return
- $.addClass QR.el, 'captcha'
- $.after $('.textarea', QR.el), $.el 'div',
- className: 'captchaimg'
- title: 'Reload'
- innerHTML: '
'
- $.after $('.captchaimg', QR.el), $.el 'div',
- className: 'captchainput'
- innerHTML: ''
- @img = $ '.captchaimg > img', QR.el
- @input = $ '.captchainput > input', QR.el
- $.on @img.parentNode, 'click', @reload
- $.on @input, 'keydown', @keydown
- $.on @challenge, 'DOMNodeInserted', => @load()
- $.sync 'captchas', (arr) => @count arr.length
- @count $.get('captchas', []).length
- # start with an uncached captcha
- @reload()
- save: ->
- return unless response = @input.value
- captchas = $.get 'captchas', []
- # Remove old captchas.
- while (captcha = captchas[0]) and captcha.time < Date.now()
- captchas.shift()
- captchas.push
- challenge: @challenge.firstChild.value
- response: response
- time: @timeout
- $.set 'captchas', captchas
- @count captchas.length
- @reload()
- load: ->
- # -1 minute to give upload some time.
- @timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE
- challenge = @challenge.firstChild.value
- @img.alt = challenge
- @img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
- @input.value = null
- count: (count) ->
- @input.placeholder = switch count
- when 0
- 'Verification (Shift + Enter to cache)'
- when 1
- 'Verification (1 cached captcha)'
- else
- "Verification (#{count} cached captchas)"
- @input.alt = count # For XTRM RICE.
- reload: (focus) ->
- # the 't' argument prevents the input from being focused
- $.unsafeWindow.Recaptcha.reload 't'
- # Focus if we meant to.
- QR.captcha.input.focus() if focus
- keydown: (e) ->
- c = QR.captcha
- if e.keyCode is 8 and not c.input.value
- c.reload()
- else if e.keyCode is 13 and e.shiftKey
- c.save()
- else
- return
- e.preventDefault()
-
- dialog: ->
- QR.el = UI.dialog 'qr', 'top:0;right:0;', """
-
-
- """
-
- # Allow only this board's supported files.
- mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) ->
- switch type
- when 'jpg'
- 'image/jpeg'
- when 'pdf'
- 'application/pdf'
- when 'swf'
- 'application/x-shockwave-flash'
- else
- "image/#{type}"
- QR.mimeTypes = mimeTypes.split ', '
- # Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
- QR.mimeTypes.push ''
- fileInput = $ 'input[type=file]', QR.el
- fileInput.max = $('input[name=MAX_FILE_SIZE]').value
- fileInput.accept = mimeTypes if $.engine isnt 'presto' # Opera's accept attribute is fucked up
-
- QR.spoiler = !!$ 'input[name=spoiler]'
- spoiler = $ '#spoilerLabel', QR.el
- spoiler.hidden = !QR.spoiler
-
- QR.charaCounter = $ '#charCount', QR.el
- ta = $ 'textarea', QR.el
-
- span = $('.move > span', QR.el)
-
- # Make a list of visible threads.
- if g.BOARD.ID is 'f'
- if g.VIEW is 'index'
- QR.threadSelector = $('select[name=filetag]').cloneNode true
- else
- QR.threadSelector = $.el 'select',
- title: 'Create a new thread / Reply to a thread'
- threads = ''
- for key, thread of g.BOARD.threads
- threads += ""
- QR.threadSelector.innerHTML = threads
- if g.VIEW is 'thread'
- QR.threadSelector.value = g.THREAD
- if QR.threadSelector
- $.prepend span, QR.threadSelector
- $.on span, 'mousedown', (e) -> e.stopPropagation()
- $.on $('#autohide', QR.el), 'change', QR.toggleHide
- $.on $('.close', QR.el), 'click', QR.close
- $.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump'
- $.on $('#addReply', QR.el), 'click', -> new QR.reply().select()
- $.on $('form', QR.el), 'submit', QR.submit
- $.on ta, 'input', -> QR.selected.el.lastChild.textContent = @value
- $.on ta, 'input', QR.characterCount
- $.on fileInput, 'change', QR.fileInput
- $.on fileInput, 'click', (e) -> if e.shiftKey then QR.selected.rmFile() or e.preventDefault()
- $.on spoiler.firstChild, 'change', -> $('input', QR.selected.el).click()
-
- new QR.reply().select()
- # save selected reply's data
- for name in ['name', 'email', 'sub', 'com']
- # The input event replaces keyup, change and paste events.
- $.on $("[name=#{name}]", QR.el), 'input', ->
- QR.selected[@name] = @value
- # Disable auto-posting if you're typing in the first reply
- # during the last 5 seconds of the cooldown.
- if QR.cooldown.auto and QR.selected is QR.replies[0] and 0 < QR.cooldown.seconds <= 5
- QR.cooldown.auto = false
-
- QR.status.input = $ 'input[type=submit]', QR.el
- QR.status()
- QR.cooldown.init()
- QR.captcha.init()
- $.add d.body, QR.el
-
- # Create a custom event when the QR dialog is first initialized.
- # Use it to extend the QR's functionalities, or for XTRM RICE.
- $.event new CustomEvent 'QRDialogCreation', null, QR.el
-
- submit: (e) ->
- e?.preventDefault()
- if QR.cooldown.seconds
- QR.cooldown.auto = !QR.cooldown.auto
- QR.status()
- return
- QR.abort()
-
- reply = QR.replies[0]
- if g.BOARD.ID is 'f' and g.VIEW is 'index'
- filetag = QR.threadSelector.value
- threadID = 'new'
- else
- threadID = QR.threadSelector.value
-
- # prevent errors
- if threadID is 'new'
- threadID = null
- if g.BOARD.ID in ['vg', 'q'] and !reply.sub
- err = 'New threads require a subject.'
- else unless reply.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
- err = 'No file selected.'
- else if g.BOARD.ID is 'f' and filetag is '9999'
- err = 'Invalid tag specified.'
- else unless reply.com or reply.file
- err = 'No file selected.'
-
- if QR.captcha.isEnabled and !err
- # get oldest valid captcha
- captchas = $.get 'captchas', []
- # remove old captchas
- while (captcha = captchas[0]) and captcha.time < Date.now()
- captchas.shift()
- if captcha = captchas.shift()
- challenge = captcha.challenge
- response = captcha.response
- else
- challenge = QR.captcha.img.alt
- if response = QR.captcha.input.value then QR.captcha.reload()
- $.set 'captchas', captchas
- QR.captcha.count captchas.length
- unless response
- err = 'No valid captcha.'
- else
- response = response.trim()
- # one-word-captcha:
- # If there's only one word, duplicate it.
- response = "#{response} #{response}" unless /\s/.test response
-
- if err
- # stop auto-posting
- QR.cooldown.auto = false
- QR.status()
- QR.error err
- return
- QR.cleanNotification()
-
- # Enable auto-posting if we have stuff to post, disable it otherwise.
- QR.cooldown.auto = QR.replies.length > 1
- if Conf['Auto Hide QR'] and not QR.cooldown.auto
- QR.hide()
- if not QR.cooldown.auto and $.x 'ancestor::div[@id="qr"]', d.activeElement
- # Unfocus the focused element if it is one within the QR and we're not auto-posting.
- d.activeElement.blur()
-
- # Starting to upload might take some time.
- # Provide some feedback that we're starting to submit.
- QR.status progress: '...'
-
- post =
- resto: threadID
- name: reply.name
- email: reply.email
- sub: reply.sub
- com: reply.com
- upfile: reply.file
- filetag: filetag
- spoiler: reply.spoiler
- textonly: textOnly
- mode: 'regist'
- pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
- recaptcha_challenge_field: challenge
- recaptcha_response_field: response
-
- callbacks =
- onload: ->
- QR.response @response
- onerror: ->
- # Connection error, or
- # CORS disabled error on www.4chan.org/banned
- QR.cooldown.auto = false
- QR.status()
- QR.error $.el 'a',
- href: '//www.4chan.org/banned',
- target: '_blank',
- textContent: 'Connection error, or you are banned.'
- opts =
- form: $.formData post
- upCallbacks:
- onload: ->
- # Upload done, waiting for response.
- QR.status progress: '...'
- onprogress: (e) ->
- # Uploading...
- QR.status progress: "#{Math.round e.loaded / e.total * 100}%"
-
- QR.ajax = $.ajax $.id('postForm').parentNode.action, callbacks, opts
-
- response: (html) ->
- tmpDoc = d.implementation.createHTMLDocument ''
- tmpDoc.documentElement.innerHTML = html
- if ban = $ '.banType', tmpDoc # banned/warning
- board = $('.board', tmpDoc).innerHTML
- err = $.el 'span', innerHTML:
- if ban.textContent.toLowerCase() is 'banned'
- "You are banned on #{board}! ;_;
" +
- "Click here to see the reason."
- else
- "You were issued a warning on #{board} as #{$('.nameBlock', tmpDoc).innerHTML}.
" +
- "Reason: #{$('.reason', tmpDoc).innerHTML}"
- else if err = tmpDoc.getElementById 'errmsg' # error!
- $('a', err)?.target = '_blank' # duplicate image link
- else if tmpDoc.title isnt 'Post successful!'
- err = 'Connection error with sys.4chan.org.'
-
- if err
- if /captcha|verification/i.test(err.textContent) or err is 'Connection error with sys.4chan.org.'
- # Remove the obnoxious 4chan Pass ad.
- if /mistyped/i.test err.textContent
- err = 'Error: You seem to have mistyped the CAPTCHA.'
- # Enable auto-post if we have some cached captchas.
- QR.cooldown.auto =
- if QR.captcha.isEnabled
- !!$.get('captchas', []).length
- else if err is 'Connection error with sys.4chan.org.'
- true
- else
- # Something must've gone terribly wrong if you get captcha errors without captchas.
- # Don't auto-post indefinitely in that case.
- false
- # Too many frequent mistyped captchas will auto-ban you!
- # On connection error, the post most likely didn't go through.
- QR.cooldown.set delay: 2
- else # stop auto-posting
- QR.cooldown.auto = false
- QR.status()
- QR.error err
- return
-
- h1 = $ 'h1', tmpDoc
- QR.lastNotification = new Notification 'success', h1.textContent, 5
-
- reply = QR.replies[0]
-
- persona = $.get 'QR.persona', {}
- persona =
- name: reply.name
- email: if /^sage$/.test reply.email then persona.email else reply.email
- sub: if Conf['Remember Subject'] then reply.sub else null
- $.set 'QR.persona', persona
-
- [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
-
- # Post/upload confirmed as successful.
- $.event new CustomEvent 'QRPostSuccessful', {
- threadID
- postID
- }, QR.el
-
- QR.cooldown.set
- post: reply
- isReply: threadID isnt '0'
-
- # Enable auto-posting if we have stuff to post, disable it otherwise.
- QR.cooldown.auto = QR.replies.length > 1
-
- if threadID is '0' # new thread
- $.open "/#{g.BOARD}/res/#{postID}"
- else if g.VIEW is 'reply' and !QR.cooldown.auto # posting from the index
- $.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}"
-
- if Conf['Persistent QR'] or QR.cooldown.auto
- reply.rm()
- else
- QR.close()
-
- QR.status()
- QR.resetFileInput()
-
- abort: ->
- QR.ajax?.abort()
- delete QR.ajax
- QR.status()
-
Menu =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu']
diff --git a/src/qr.coffee b/src/qr.coffee
new file mode 100644
index 000000000..1d04510b8
--- /dev/null
+++ b/src/qr.coffee
@@ -0,0 +1,796 @@
+QR =
+ init: ->
+ return if g.VIEW is 'catalog' or !Conf['Quick Reply']
+
+ if Conf['Hide Original Post Form']
+ $.addClass doc, 'hide-original-post-form'
+
+ link = $.el 'a',
+ className: 'qr-shortcut'
+ textContent: 'Quick Reply'
+ href: 'javascript:;'
+ $.on link, 'click', ->
+ Header.menu.close()
+ QR.open()
+ if g.BOARD.ID is 'f'
+ if g.VIEW is 'index'
+ QR.threadSelector.value = '9999'
+ else if g.VIEW is 'thread'
+ QR.threadSelector.value = g.THREAD
+ else
+ QR.threadSelector.value = 'new'
+ $('textarea', QR.el).focus()
+ $.event 'AddMenuEntry',
+ type: 'header'
+ el: link
+
+ $.on d, 'dragover', QR.dragOver
+ $.on d, 'drop', QR.dropFile
+ $.on d, 'dragstart dragend', QR.drag
+ $.on d, '4chanXInitFinished', ->
+ return unless Conf['Persistent QR']
+ QR.open()
+ QR.hide() if Conf['Auto Hide QR']
+
+ Post::callbacks.push
+ name: 'Quick Reply'
+ cb: @node
+
+ node: ->
+ $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
+
+ open: ->
+ if QR.el
+ QR.el.hidden = false
+ QR.unhide()
+ return
+ try
+ QR.dialog()
+ catch err
+ delete QR.el
+ Main.handleErrors
+ message: 'Quick Reply dialog creation crashed.'
+ error: err
+ close: ->
+ QR.el.hidden = true
+ QR.abort()
+ d.activeElement.blur()
+ $.rmClass QR.el, 'dump'
+ for i in QR.replies
+ QR.replies[0].rm()
+ QR.cooldown.auto = false
+ QR.status()
+ QR.resetFileInput()
+ if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked
+ spoiler.click()
+ QR.cleanNotification()
+ hide: ->
+ d.activeElement.blur()
+ $.addClass QR.el, 'autohide'
+ $.id('autohide').checked = true
+ unhide: ->
+ $.rmClass QR.el, 'autohide'
+ $.id('autohide').checked = false
+ toggleHide: ->
+ if @checked
+ QR.hide()
+ else
+ QR.unhide()
+
+ error: (err) ->
+ QR.open()
+ if typeof err is 'string'
+ el = $.tn err
+ else
+ el = err
+ el.removeAttribute 'style'
+ if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
+ # Focus the captcha input on captcha error.
+ $('[autocomplete]', QR.el).focus()
+ alert el.textContent if d.hidden
+ QR.lastNotification = new Notification 'warning', el
+ cleanNotification: ->
+ QR.lastNotification?.close()
+ delete QR.lastNotification
+
+ status: (data={}) ->
+ return unless QR.el
+ if g.dead # XXX
+ value = 404
+ disabled = true
+ QR.cooldown.auto = false
+ value = data.progress or QR.cooldown.seconds or value
+ {input} = QR.status
+ input.value =
+ if QR.cooldown.auto
+ if value then "Auto #{value}" else 'Auto'
+ else
+ value or 'Submit'
+ input.disabled = disabled or false
+
+ cooldown:
+ init: ->
+ QR.cooldown.types =
+ thread: switch g.BOARD
+ when 'q' then 86400
+ when 'b', 'soc', 'r9k' then 600
+ else 300
+ sage: if g.BOARD is 'q' then 600 else 60
+ file: if g.BOARD is 'q' then 300 else 30
+ post: if g.BOARD is 'q' then 60 else 30
+ QR.cooldown.cooldowns = $.get "#{g.BOARD}.cooldown", {}
+ QR.cooldown.start()
+ $.sync "#{g.BOARD}.cooldown", QR.cooldown.sync
+ start: ->
+ return if QR.cooldown.isCounting
+ QR.cooldown.isCounting = true
+ QR.cooldown.count()
+ sync: (cooldowns) ->
+ # Add each cooldowns, don't overwrite everything in case we
+ # still need to prune one in the current tab to auto-post.
+ for id of cooldowns
+ QR.cooldown.cooldowns[id] = cooldowns[id]
+ QR.cooldown.start()
+ set: (data) ->
+ start = Date.now()
+ if data.delay
+ cooldown = delay: data.delay
+ else
+ isSage = /sage/i.test data.post.email
+ hasFile = !!data.post.file
+ isReply = data.isReply
+ type =
+ unless isReply
+ 'thread'
+ else if isSage
+ 'sage'
+ else if hasFile
+ 'file'
+ else
+ 'post'
+ cooldown =
+ isReply: isReply
+ isSage: isSage
+ hasFile: hasFile
+ timeout: start + QR.cooldown.types[type] * $.SECOND
+ QR.cooldown.cooldowns[start] = cooldown
+ $.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns
+ QR.cooldown.start()
+ unset: (id) ->
+ delete QR.cooldown.cooldowns[id]
+ $.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns
+ count: ->
+ if Object.keys(QR.cooldown.cooldowns).length
+ setTimeout QR.cooldown.count, 1000
+ else
+ $.delete "#{g.BOARD}.cooldown"
+ delete QR.cooldown.isCounting
+ delete QR.cooldown.seconds
+ QR.status()
+ return
+
+ isReply =
+ if g.BOARD.ID is 'f' and g.VIEW is 'thread'
+ true
+ else
+ QR.threadSelector.value isnt 'new'
+ if isReply
+ post = QR.replies[0]
+ isSage = /sage/i.test post.email
+ hasFile = !!post.file
+ now = Date.now()
+ seconds = null
+ {types, cooldowns} = QR.cooldown
+
+ for start, cooldown of cooldowns
+ if 'delay' of cooldown
+ if cooldown.delay
+ seconds = Math.max seconds, cooldown.delay--
+ else
+ seconds = Math.max seconds, 0
+ QR.cooldown.unset start
+ continue
+
+ if isReply is cooldown.isReply
+ # Only cooldowns relevant to this post can set the seconds value.
+ # Unset outdated cooldowns that can no longer impact us.
+ type =
+ unless isReply
+ 'thread'
+ else if isSage and cooldown.isSage
+ 'sage'
+ else if hasFile and cooldown.hasFile
+ 'file'
+ else
+ 'post'
+ elapsed = Math.floor (now - start) / 1000
+ if elapsed >= 0 # clock changed since then?
+ seconds = Math.max seconds, types[type] - elapsed
+ unless start <= now <= cooldown.timeout
+ QR.cooldown.unset start
+
+ # Update the status when we change posting type.
+ # Don't get stuck at some random number.
+ # Don't interfere with progress status updates.
+ update = seconds isnt null or !!QR.cooldown.seconds
+ QR.cooldown.seconds = seconds
+ QR.status() if update
+ QR.submit() if seconds is 0 and QR.cooldown.auto
+
+ quote: (e) ->
+ e?.preventDefault()
+ QR.open()
+ ta = $ 'textarea', QR.el
+ if QR.threadSelector and !ta.value and g.BOARD.ID isnt 'f'
+ QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..]
+ # Make sure we get the correct number, even with XXX censors
+ post = Get.postFromRoot $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', @
+ text = ">>#{post}\n"
+
+ sel = d.getSelection()
+ selectionRoot = $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', sel.anchorNode
+ if (s = sel.toString().trim()) and post.nodes.root is selectionRoot
+ # XXX Opera doesn't retain `\n`s?
+ s = s.replace /\n/g, '\n>'
+ text += ">#{s}\n"
+
+ caretPos = ta.selectionStart
+ # Replace selection for text.
+ ta.value = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..]
+ # Move the caret to the end of the new quote.
+ range = caretPos + text.length
+ ta.setSelectionRange range, range
+ ta.focus()
+
+ # Fire the 'input' event
+ ta.dispatchEvent new Event 'input'
+
+ characterCount: ->
+ counter = QR.charaCounter
+ count = @textLength
+ counter.textContent = count
+ counter.hidden = count < 1000
+ (if count > 1500 then $.addClass else $.rmClass) counter, 'warning'
+
+ drag: (e) ->
+ # Let it drag anything from the page.
+ toggle = if e.type is 'dragstart' then $.off else $.on
+ toggle d, 'dragover', QR.dragOver
+ toggle d, 'drop', QR.dropFile
+ dragOver: (e) ->
+ e.preventDefault()
+ e.dataTransfer.dropEffect = 'copy' # cursor feedback
+ dropFile: (e) ->
+ # Let it only handle files from the desktop.
+ return unless e.dataTransfer.files.length
+ e.preventDefault()
+ QR.open()
+ QR.fileInput.call e.dataTransfer
+ $.addClass QR.el, 'dump'
+ fileInput: ->
+ QR.cleanNotification()
+ # Set or change current reply's file.
+ if @files.length is 1
+ file = @files[0]
+ if file.size > @max
+ QR.error 'File too large.'
+ QR.resetFileInput()
+ else if -1 is QR.mimeTypes.indexOf file.type
+ QR.error 'Unsupported file type.'
+ QR.resetFileInput()
+ else
+ QR.selected.setFile file
+ return
+ # Create new replies with these files.
+ for file in @files
+ if file.size > @max
+ QR.error "File #{file.name} is too large."
+ break
+ else if -1 is QR.mimeTypes.indexOf file.type
+ QR.error "#{file.name}: Unsupported file type."
+ break
+ unless QR.replies[QR.replies.length - 1].file
+ # set last reply's file
+ QR.replies[QR.replies.length - 1].setFile file
+ else
+ new QR.reply().setFile file
+ $.addClass QR.el, 'dump'
+ QR.resetFileInput() # reset input
+ resetFileInput: ->
+ $('[type=file]', QR.el).value = null
+
+ replies: []
+ reply: class
+ constructor: ->
+ # set values, or null, to avoid 'undefined' values in inputs
+ prev = QR.replies[QR.replies.length-1]
+ persona = $.get 'QR.persona', {}
+ @name = if prev then prev.name else persona.name or null
+ @email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null
+ @sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null
+ @spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false
+ @com = null
+
+ @el = $.el 'a',
+ className: 'qrpreview'
+ draggable: true
+ href: 'javascript:;'
+ innerHTML: '×'
+ $('input', @el).checked = @spoiler
+ $.on @el, 'click', => @select()
+ $.on $('.remove', @el), 'click', (e) =>
+ e.stopPropagation()
+ @rm()
+ $.on $('label', @el), 'click', (e) => e.stopPropagation()
+ $.on $('input', @el), 'change', (e) =>
+ @spoiler = e.target.checked
+ $.id('spoiler').checked = @spoiler if @el.id is 'selected'
+ $.before $('#addReply', QR.el), @el
+
+ $.on @el, 'dragstart', @dragStart
+ $.on @el, 'dragenter', @dragEnter
+ $.on @el, 'dragleave', @dragLeave
+ $.on @el, 'dragover', @dragOver
+ $.on @el, 'dragend', @dragEnd
+ $.on @el, 'drop', @drop
+
+ QR.replies.push @
+ setFile: (@file) ->
+ @el.title = "#{file.name} (#{$.bytesToString file.size})"
+ $('label', @el).hidden = false if QR.spoiler
+ unless /^image/.test file.type
+ @el.style.backgroundImage = null
+ return
+ # XXX Opera does not support window.URL
+ return unless url = window.URL or window.webkitURL
+ url.revokeObjectURL @url
+
+ # Create a redimensioned thumbnail.
+ fileUrl = url.createObjectURL file
+ img = $.el 'img'
+
+ $.on img, 'load', =>
+ # Generate thumbnails only if they're really big.
+ # Resized pictures through canvases look like ass,
+ # so we generate thumbnails `s` times bigger then expected
+ # to avoid crappy resized quality.
+ s = 90*3
+ if img.height < s or img.width < s
+ @url = fileUrl
+ @el.style.backgroundImage = "url(#{@url})"
+ return
+ if img.height <= img.width
+ img.width = s / img.height * img.width
+ img.height = s
+ else
+ img.height = s / img.width * img.height
+ img.width = s
+ c = $.el 'canvas'
+ c.height = img.height
+ c.width = img.width
+ c.getContext('2d').drawImage img, 0, 0, img.width, img.height
+ # Support for toBlob fucking when?
+ data = atob c.toDataURL().split(',')[1]
+
+ # DataUrl to Binary code from Aeosynth's 4chan X repo
+ l = data.length
+ ui8a = new Uint8Array l
+ for i in [0...l]
+ ui8a[i] = data.charCodeAt i
+
+ @url = url.createObjectURL new Blob [ui8a], type: 'image/png'
+ @el.style.backgroundImage = "url(#{@url})"
+ url.revokeObjectURL? fileUrl
+
+ img.src = fileUrl
+ rmFile: ->
+ QR.resetFileInput()
+ delete @file
+ @el.title = null
+ @el.style.backgroundImage = null
+ $('label', @el).hidden = true if QR.spoiler
+ (window.URL or window.webkitURL).revokeObjectURL? @url
+ select: ->
+ QR.selected?.el.id = null
+ QR.selected = @
+ @el.id = 'selected'
+ # Scroll the list to center the focused reply.
+ rectEl = @el.getBoundingClientRect()
+ rectList = @el.parentNode.getBoundingClientRect()
+ @el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2
+ # Load this reply's values.
+ for data in ['name', 'email', 'sub', 'com']
+ $("[name=#{data}]", QR.el).value = @[data]
+ QR.characterCount.call $ 'textarea', QR.el
+ $('#spoiler', QR.el).checked = @spoiler
+ dragStart: ->
+ $.addClass @, 'drag'
+ dragEnter: ->
+ $.addClass @, 'over'
+ dragLeave: ->
+ $.rmClass @, 'over'
+ dragOver: (e) ->
+ e.preventDefault()
+ e.dataTransfer.dropEffect = 'move'
+ drop: ->
+ el = $ '.drag', @parentNode
+ index = (el) -> Array::slice.call(el.parentNode.children).indexOf el
+ oldIndex = index el
+ newIndex = index @
+ if oldIndex < newIndex
+ $.after @, el
+ else
+ $.before @, el
+ reply = QR.replies.splice(oldIndex, 1)[0]
+ QR.replies.splice newIndex, 0, reply
+ dragEnd: ->
+ $.rmClass @, 'drag'
+ if el = $ '.over', @parentNode
+ $.rmClass el, 'over'
+ rm: ->
+ QR.resetFileInput()
+ $.rm @el
+ index = QR.replies.indexOf @
+ if QR.replies.length is 1
+ new QR.reply().select()
+ else if @el.id is 'selected'
+ (QR.replies[index-1] or QR.replies[index+1]).select()
+ QR.replies.splice index, 1
+ (window.URL or window.webkitURL)?.revokeObjectURL @url
+
+ captcha:
+ init: ->
+ return if -1 isnt d.cookie.indexOf 'pass_enabled='
+ return unless @isEnabled = !!$.id 'captchaFormPart'
+ if $.id 'recaptcha_challenge_field_holder'
+ @ready()
+ else
+ @onready = => @ready()
+ $.on $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready
+ ready: ->
+ if @challenge = $.id 'recaptcha_challenge_field_holder'
+ $.off $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready
+ delete @onready
+ else
+ return
+ $.addClass QR.el, 'captcha'
+ $.after $('.textarea', QR.el), $.el 'div',
+ className: 'captchaimg'
+ title: 'Reload'
+ innerHTML: '
'
+ $.after $('.captchaimg', QR.el), $.el 'div',
+ className: 'captchainput'
+ innerHTML: ''
+ @img = $ '.captchaimg > img', QR.el
+ @input = $ '.captchainput > input', QR.el
+ $.on @img.parentNode, 'click', @reload
+ $.on @input, 'keydown', @keydown
+ $.on @challenge, 'DOMNodeInserted', => @load()
+ $.sync 'captchas', (arr) => @count arr.length
+ @count $.get('captchas', []).length
+ # start with an uncached captcha
+ @reload()
+ save: ->
+ return unless response = @input.value
+ captchas = $.get 'captchas', []
+ # Remove old captchas.
+ while (captcha = captchas[0]) and captcha.time < Date.now()
+ captchas.shift()
+ captchas.push
+ challenge: @challenge.firstChild.value
+ response: response
+ time: @timeout
+ $.set 'captchas', captchas
+ @count captchas.length
+ @reload()
+ load: ->
+ # -1 minute to give upload some time.
+ @timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE
+ challenge = @challenge.firstChild.value
+ @img.alt = challenge
+ @img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
+ @input.value = null
+ count: (count) ->
+ @input.placeholder = switch count
+ when 0
+ 'Verification (Shift + Enter to cache)'
+ when 1
+ 'Verification (1 cached captcha)'
+ else
+ "Verification (#{count} cached captchas)"
+ @input.alt = count # For XTRM RICE.
+ reload: (focus) ->
+ # the 't' argument prevents the input from being focused
+ $.unsafeWindow.Recaptcha.reload 't'
+ # Focus if we meant to.
+ QR.captcha.input.focus() if focus
+ keydown: (e) ->
+ c = QR.captcha
+ if e.keyCode is 8 and not c.input.value
+ c.reload()
+ else if e.keyCode is 13 and e.shiftKey
+ c.save()
+ else
+ return
+ e.preventDefault()
+
+ dialog: ->
+ QR.el = UI.dialog 'qr', 'top:0;right:0;', """
+
+
+ """
+
+ # Allow only this board's supported files.
+ mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) ->
+ switch type
+ when 'jpg'
+ 'image/jpeg'
+ when 'pdf'
+ 'application/pdf'
+ when 'swf'
+ 'application/x-shockwave-flash'
+ else
+ "image/#{type}"
+ QR.mimeTypes = mimeTypes.split ', '
+ # Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
+ QR.mimeTypes.push ''
+ fileInput = $ 'input[type=file]', QR.el
+ fileInput.max = $('input[name=MAX_FILE_SIZE]').value
+ fileInput.accept = mimeTypes if $.engine isnt 'presto' # Opera's accept attribute is fucked up
+
+ QR.spoiler = !!$ 'input[name=spoiler]'
+ spoiler = $ '#spoilerLabel', QR.el
+ spoiler.hidden = !QR.spoiler
+
+ QR.charaCounter = $ '#charCount', QR.el
+ ta = $ 'textarea', QR.el
+
+ span = $('.move > span', QR.el)
+
+ # Make a list of visible threads.
+ if g.BOARD.ID is 'f'
+ if g.VIEW is 'index'
+ QR.threadSelector = $('select[name=filetag]').cloneNode true
+ else
+ QR.threadSelector = $.el 'select',
+ title: 'Create a new thread / Reply to a thread'
+ threads = ''
+ for key, thread of g.BOARD.threads
+ threads += ""
+ QR.threadSelector.innerHTML = threads
+ if g.VIEW is 'thread'
+ QR.threadSelector.value = g.THREAD
+ if QR.threadSelector
+ $.prepend span, QR.threadSelector
+ $.on span, 'mousedown', (e) -> e.stopPropagation()
+ $.on $('#autohide', QR.el), 'change', QR.toggleHide
+ $.on $('.close', QR.el), 'click', QR.close
+ $.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump'
+ $.on $('#addReply', QR.el), 'click', -> new QR.reply().select()
+ $.on $('form', QR.el), 'submit', QR.submit
+ $.on ta, 'input', -> QR.selected.el.lastChild.textContent = @value
+ $.on ta, 'input', QR.characterCount
+ $.on fileInput, 'change', QR.fileInput
+ $.on fileInput, 'click', (e) -> if e.shiftKey then QR.selected.rmFile() or e.preventDefault()
+ $.on spoiler.firstChild, 'change', -> $('input', QR.selected.el).click()
+
+ new QR.reply().select()
+ # save selected reply's data
+ for name in ['name', 'email', 'sub', 'com']
+ # The input event replaces keyup, change and paste events.
+ $.on $("[name=#{name}]", QR.el), 'input', ->
+ QR.selected[@name] = @value
+ # Disable auto-posting if you're typing in the first reply
+ # during the last 5 seconds of the cooldown.
+ if QR.cooldown.auto and QR.selected is QR.replies[0] and 0 < QR.cooldown.seconds <= 5
+ QR.cooldown.auto = false
+
+ QR.status.input = $ 'input[type=submit]', QR.el
+ QR.status()
+ QR.cooldown.init()
+ QR.captcha.init()
+ $.add d.body, QR.el
+
+ # Create a custom event when the QR dialog is first initialized.
+ # Use it to extend the QR's functionalities, or for XTRM RICE.
+ $.event new CustomEvent 'QRDialogCreation', null, QR.el
+
+ submit: (e) ->
+ e?.preventDefault()
+ if QR.cooldown.seconds
+ QR.cooldown.auto = !QR.cooldown.auto
+ QR.status()
+ return
+ QR.abort()
+
+ reply = QR.replies[0]
+ if g.BOARD.ID is 'f' and g.VIEW is 'index'
+ filetag = QR.threadSelector.value
+ threadID = 'new'
+ else
+ threadID = QR.threadSelector.value
+
+ # prevent errors
+ if threadID is 'new'
+ threadID = null
+ if g.BOARD.ID in ['vg', 'q'] and !reply.sub
+ err = 'New threads require a subject.'
+ else unless reply.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
+ err = 'No file selected.'
+ else if g.BOARD.ID is 'f' and filetag is '9999'
+ err = 'Invalid tag specified.'
+ else unless reply.com or reply.file
+ err = 'No file selected.'
+
+ if QR.captcha.isEnabled and !err
+ # get oldest valid captcha
+ captchas = $.get 'captchas', []
+ # remove old captchas
+ while (captcha = captchas[0]) and captcha.time < Date.now()
+ captchas.shift()
+ if captcha = captchas.shift()
+ challenge = captcha.challenge
+ response = captcha.response
+ else
+ challenge = QR.captcha.img.alt
+ if response = QR.captcha.input.value then QR.captcha.reload()
+ $.set 'captchas', captchas
+ QR.captcha.count captchas.length
+ unless response
+ err = 'No valid captcha.'
+ else
+ response = response.trim()
+ # one-word-captcha:
+ # If there's only one word, duplicate it.
+ response = "#{response} #{response}" unless /\s/.test response
+
+ if err
+ # stop auto-posting
+ QR.cooldown.auto = false
+ QR.status()
+ QR.error err
+ return
+ QR.cleanNotification()
+
+ # Enable auto-posting if we have stuff to post, disable it otherwise.
+ QR.cooldown.auto = QR.replies.length > 1
+ if Conf['Auto Hide QR'] and not QR.cooldown.auto
+ QR.hide()
+ if not QR.cooldown.auto and $.x 'ancestor::div[@id="qr"]', d.activeElement
+ # Unfocus the focused element if it is one within the QR and we're not auto-posting.
+ d.activeElement.blur()
+
+ # Starting to upload might take some time.
+ # Provide some feedback that we're starting to submit.
+ QR.status progress: '...'
+
+ post =
+ resto: threadID
+ name: reply.name
+ email: reply.email
+ sub: reply.sub
+ com: reply.com
+ upfile: reply.file
+ filetag: filetag
+ spoiler: reply.spoiler
+ textonly: textOnly
+ mode: 'regist'
+ pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
+ recaptcha_challenge_field: challenge
+ recaptcha_response_field: response
+
+ callbacks =
+ onload: ->
+ QR.response @response
+ onerror: ->
+ # Connection error, or
+ # CORS disabled error on www.4chan.org/banned
+ QR.cooldown.auto = false
+ QR.status()
+ QR.error $.el 'a',
+ href: '//www.4chan.org/banned',
+ target: '_blank',
+ textContent: 'Connection error, or you are banned.'
+ opts =
+ form: $.formData post
+ upCallbacks:
+ onload: ->
+ # Upload done, waiting for response.
+ QR.status progress: '...'
+ onprogress: (e) ->
+ # Uploading...
+ QR.status progress: "#{Math.round e.loaded / e.total * 100}%"
+
+ QR.ajax = $.ajax $.id('postForm').parentNode.action, callbacks, opts
+
+ response: (html) ->
+ tmpDoc = d.implementation.createHTMLDocument ''
+ tmpDoc.documentElement.innerHTML = html
+ if ban = $ '.banType', tmpDoc # banned/warning
+ board = $('.board', tmpDoc).innerHTML
+ err = $.el 'span', innerHTML:
+ if ban.textContent.toLowerCase() is 'banned'
+ "You are banned on #{board}! ;_;
" +
+ "Click here to see the reason."
+ else
+ "You were issued a warning on #{board} as #{$('.nameBlock', tmpDoc).innerHTML}.
" +
+ "Reason: #{$('.reason', tmpDoc).innerHTML}"
+ else if err = tmpDoc.getElementById 'errmsg' # error!
+ $('a', err)?.target = '_blank' # duplicate image link
+ else if tmpDoc.title isnt 'Post successful!'
+ err = 'Connection error with sys.4chan.org.'
+
+ if err
+ if /captcha|verification/i.test(err.textContent) or err is 'Connection error with sys.4chan.org.'
+ # Remove the obnoxious 4chan Pass ad.
+ if /mistyped/i.test err.textContent
+ err = 'Error: You seem to have mistyped the CAPTCHA.'
+ # Enable auto-post if we have some cached captchas.
+ QR.cooldown.auto =
+ if QR.captcha.isEnabled
+ !!$.get('captchas', []).length
+ else if err is 'Connection error with sys.4chan.org.'
+ true
+ else
+ # Something must've gone terribly wrong if you get captcha errors without captchas.
+ # Don't auto-post indefinitely in that case.
+ false
+ # Too many frequent mistyped captchas will auto-ban you!
+ # On connection error, the post most likely didn't go through.
+ QR.cooldown.set delay: 2
+ else # stop auto-posting
+ QR.cooldown.auto = false
+ QR.status()
+ QR.error err
+ return
+
+ h1 = $ 'h1', tmpDoc
+ QR.lastNotification = new Notification 'success', h1.textContent, 5
+
+ reply = QR.replies[0]
+
+ persona = $.get 'QR.persona', {}
+ persona =
+ name: reply.name
+ email: if /^sage$/.test reply.email then persona.email else reply.email
+ sub: if Conf['Remember Subject'] then reply.sub else null
+ $.set 'QR.persona', persona
+
+ [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
+
+ # Post/upload confirmed as successful.
+ $.event new CustomEvent 'QRPostSuccessful', {
+ threadID
+ postID
+ }, QR.el
+
+ QR.cooldown.set
+ post: reply
+ isReply: threadID isnt '0'
+
+ # Enable auto-posting if we have stuff to post, disable it otherwise.
+ QR.cooldown.auto = QR.replies.length > 1
+
+ if threadID is '0' # new thread
+ $.open "/#{g.BOARD}/res/#{postID}"
+ else if g.VIEW is 'reply' and !QR.cooldown.auto # posting from the index
+ $.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}"
+
+ if Conf['Persistent QR'] or QR.cooldown.auto
+ reply.rm()
+ else
+ QR.close()
+
+ QR.status()
+ QR.resetFileInput()
+
+ abort: ->
+ QR.ajax?.abort()
+ delete QR.ajax
+ QR.status()