Quick Reply

This commit is contained in:
Zixaphir 2015-01-09 23:25:47 -07:00
parent 4a0377dedf
commit 13563ffcda
6 changed files with 344 additions and 811 deletions

View File

@ -9101,10 +9101,19 @@
QR = {
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'],
init: function() {
var con, sc;
var con, noscript, sc;
this.db = new DataBoard('yourPosts');
this.posts = [];
if (g.VIEW === 'archive') {
return;
}
$.globalEval('document.documentElement.dataset.jsEnabled = true;');
noscript = Conf['Force Noscript Captcha'] || !doc.dataset.jsEnabled;
this.captcha = Captcha[noscript ? 'noscript' : 'v2'];
$.on(d, '4chanXInitFinished', this.initReady);
window.addEventListener('focus', this.focus, true);
window.addEventListener('blur', this.focus, true);
$.on(d, 'click', this.focus);
Post.callbacks.push({
name: 'Quick Reply',
cb: this.node
@ -9154,6 +9163,7 @@
if (!QR.postingIsEnabled) {
return;
}
$.on(d, 'paste', QR.paste);
$.on(d, 'dragover', QR.dragOver);
$.on(d, 'drop', QR.dropFile);
$.on(d, 'dragstart dragend', QR.drag);
@ -9163,12 +9173,17 @@
return;
}
QR.open();
if (Conf['Auto-Hide QR']) {
if (Conf['Auto Hide QR']) {
return QR.hide();
}
},
statusCheck: function() {
if (g.DEAD) {
var thread;
if (!QR.nodes) {
return;
}
thread = QR.posts[0].thread;
if (thread !== 'new' && g.threads["" + g.BOARD + "." + thread].isDead) {
return QR.abort();
} else {
return QR.status();
@ -9196,9 +9211,11 @@
open: function() {
var err;
if (QR.nodes) {
if (QR.nodes.el.hidden) {
QR.captcha.setup();
}
QR.nodes.el.hidden = false;
QR.unhide();
QR.captcha.setup();
return;
}
try {
@ -9235,14 +9252,34 @@
QR.status();
return QR.captcha.destroy();
},
focusin: function() {
if ($.hasClass(QR.nodes.el, 'autohide') && !$.hasClass(QR.nodes.el, 'focus')) {
QR.captcha.setup();
}
return $.addClass(QR.nodes.el, 'focus');
focus: function() {
return $.queueTask(function() {
var focus;
if (!QR.nodes) {
return;
}
if (!$$('.goog-bubble-content > iframe').some(function(el) {
return el.getBoundingClientRect().top >= 0;
})) {
focus = d.activeElement && QR.nodes.el.contains(d.activeElement);
$[focus ? 'addClass' : 'rmClass'](QR.nodes.el, 'focus');
}
if (typeof chrome !== "undefined" && chrome !== null) {
if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') {
QR.scrollY = window.scrollY;
return $.on(d, 'scroll', QR.scrollLock);
} else {
return $.off(d, 'scroll', QR.scrollLock);
}
}
});
},
focusout: function() {
return $.rmClass(QR.nodes.el, 'focus');
scrollLock: function(e) {
if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') {
return window.scroll(window.scrollX, QR.scrollY);
} else {
return $.off(d, 'scroll', QR.scrollLock);
}
},
hide: function() {
d.activeElement.blur();
@ -9271,8 +9308,10 @@
}
if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
QR.captcha.setup(true);
QR.captcha.notify(el);
} else {
QR.notify(el);
}
QR.notify(el);
if (d.hidden) {
return alert(el.textContent);
}
@ -9328,7 +9367,7 @@
}
sel = d.getSelection();
post = Get.postFromNode(this);
text = ">>" + post + "\n";
text = post.board.ID === g.BOARD.ID ? ">>" + post + "\n" : ">>>/" + post.board + "/" + post + "\n";
if (sel.toString().trim() && post === Get.postFromNode(sel.anchorNode)) {
range = sel.getRangeAt(0);
frag = range.cloneContents();
@ -9557,16 +9596,17 @@
}
list.value = g.VIEW === 'thread' ? g.THREADID : 'new';
if ($.hasClass(list, 'riced')) {
return list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent;
list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent;
}
return (g.VIEW === 'thread' ? $.addClass : $.rmClass)(QR.nodes.el, 'reply-to-thread');
},
dialog: function() {
var dialog, elm, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode;
var dialog, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode;
QR.nodes = nodes = {
el: dialog = UI.dialog('qr', 'top:0;right:0;')
};
$.extend(dialog, {
innerHTML: "<div id=qrtab class=move>\r<input type=checkbox id=autohide title=Auto-hide>\r<div id=qr-thread-select>\r<select data-name=thread title='Create a new thread / Reply'>\r<option value=new>New thread</option>\r</select>\r</div>\r<a href=javascript:; class='close fa' title=Close>\\uf00d</a>\r</div>\r<form>\r<div class=persona>\r<input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1>\r<input name=email data-name=email list=\"list-email\" placeholder=Options class=field size=1>\r<input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1> \r</div>\r<div class=textarea>\r<textarea data-name=com placeholder=Comment class=field></textarea>\r<span id=char-count></span>\r</div>\r<div id=dump-list-container>\r<div id=dump-list></div>\r<a id=add-post href=javascript:; title=\"Add a post\">+</a>\r</div>\r<div id=file-n-submit>\r<span id=qr-filename-container class=field tabindex=0>\r<span id=qr-no-file>No selected file</span>\r<input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\">\r<span id=qr-extras-container>\r<label id=qr-spoiler-label>\r<input type=checkbox id=qr-file-spoiler title='Spoiler image'>\r</label>\r<span class=description>Spoiler</span>\r<a id=url-button><i class=\"fa\">\\uf0c1</i></a>\r<span class=description>Post from URL</span>\r<a id=dump-button title='Dump list'>+</a>\r<span class=description>Dump</span>\r<a id=qr-filerm href=javascript:; title='Remove file' class=fa>\\uf00d</a>\r<span class=description>Remove File</span>\r</span>\r</span>\r<input type=submit>\r</div>\r<input type=file multiple>\r</form>\r<datalist id=\"list-name\"></datalist>\r<datalist id=\"list-email\"></datalist>\r<datalist id=\"list-sub\"></datalist>\r"
innerHTML: "<div id=qrtab class=move>\r<input type=checkbox id=autohide title=Auto-hide>\r<div id=qr-thread-select>\r<select data-name=thread title='Create a new thread / Reply'>\r<option value=new>New thread</option>\r</select>\r</div>\r<a href=javascript:; class='close fa' title=Close>\\uf00d</a>\r</div>\r<form>\r<div class=persona>\r<input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1>\r<input name=email data-name=email list=\"list-email\" placeholder=Options class=field size=1>\r<input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1> \r</div>\r<div class=textarea>\r<textarea data-name=com placeholder=Comment class=field></textarea>\r<span id=char-count></span>\r</div>\r<div id=dump-list-container>\r<div id=dump-list></div>\r<a id=add-post href=javascript:; title=\"Add a post\">+</a>\r</div>\r<div id=file-n-submit>\r<span id=qr-filename-container class=field tabindex=0>\r<span id=qr-no-file>No selected file</span>\r<input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\">\r<span id=qr-extras-container>\r<label id=qr-spoiler-label>\r<input type=checkbox id=qr-file-spoiler title='Spoiler image'>\r</label>\r<span class=description>Spoiler</span>\r<a id=url-button><i class=\"fa\">\\uf0c1</i></a>\r<span class=description>Post from URL</span>\r<a id=dump-button title='Dump list'>+</a>\r<span class=description>Dump</span>\r<a id=qr-filerm href=javascript:; title='Remove file' class=fa>\\uf00d</a>\r<span class=description>Remove File</span>\r</span>\r</span>\r<input type=submit>\r</div>\r<input type=file multiple>\r</form>\r<datalist id=\"list-name\"></datalist>\r<datalist id=\"list-email\"></datalist>\r<datalist id=\"list-sub\"></datalist>\r"
});
setNode = function(name, query) {
return nodes[name] = $(query, dialog);
@ -9606,6 +9646,14 @@
QR.max_size_video = 3145728;
QR.max_width_video = QR.max_height_video = 2048;
QR.max_duration_video = 120;
if (Conf['Show New Thread Option in Threads']) {
$.addClass(QR.nodes.el, 'show-new-thread-option');
}
if (Conf['Show Name and Subject']) {
$.addClass(QR.nodes.name, 'force-show');
$.addClass(QR.nodes.sub, 'force-show');
QR.nodes.email.placeholder = 'E-mail';
}
QR.forcedAnon = !!$('form[name="post"] input[name="name"][type="hidden"]');
if (QR.forcedAnon) {
$.addClass(QR.nodes.el, 'forced-anon');
@ -9622,20 +9670,15 @@
}
if (g.BOARD.ID === 'f' && g.VIEW !== 'thread') {
nodes.flashTag = $.el('select', {
name: 'filetag',
innerHTML: "<option value=0>Hentai</option>\n<option value=6>Porn</option>\n<option value=1>Japanese</option>\n<option value=2>Anime</option>\n<option value=3>Game</option>\n<option value=5>Loop</option>\n<option value=4 selected>Other</option>"
});
name: 'filetag'
}, $.extend(nodes.flashTag, {
innerHTML: "<option value=\"0\">Hentai</option><option value=\"6\">Porn</option><option value=\"1\">Japanese</option><option value=\"2\">Anime</option><option value=\"3\">Game</option><option value=\"5\">Loop</option><option value=\"4\" selected>Other</option>"
}));
nodes.flashTag.dataset["default"] = '4';
$.add(nodes.form, nodes.flashTag);
}
QR.flagsInput();
$.on(nodes.filename.parentNode, 'click keydown', QR.openFileInput);
items = $$('*', QR.nodes.el);
i = 0;
while (elm = items[i++]) {
$.on(elm, 'blur', QR.focusout);
$.on(elm, 'focus', QR.focusin);
}
$.on(nodes.autohide, 'change', QR.toggleHide);
$.on(nodes.close, 'click', QR.close);
$.on(nodes.dumpButton, 'click', function() {
@ -9664,7 +9707,7 @@
while (name = items[i++]) {
$.on(nodes[name], 'mouseover', QR.mouseover);
}
items = ['name', 'email', 'sub', 'com', 'filename', 'flag'];
items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag'];
i = 0;
save = function() {
return QR.selected.save(this);
@ -9734,25 +9777,18 @@
return select;
},
flagsInput: function() {
var flag, nodes;
var nodes;
nodes = QR.nodes;
if (!nodes) {
return;
}
if (nodes.flag) {
$.rm(nodes.flag);
delete nodes.flag;
}
if (g.BOARD.ID === 'pol') {
flag = QR.flags();
flag.dataset.name = 'flag';
flag.dataset["default"] = '0';
nodes.flag = flag;
return $.add(nodes.form, flag);
return delete nodes.flag;
}
},
submit: function(e) {
var err, extra, filetag, formData, options, post, response, textOnly, thread, threadID;
var captcha, cb, err, extra, filetag, formData, options, post, textOnly, thread, threadID;
if (e != null) {
e.preventDefault();
}
@ -9787,8 +9823,8 @@
err = 'Max limit of image replies has been reached.';
}
if (QR.captcha.isEnabled && !err) {
response = QR.captcha.getOne();
if (!response) {
captcha = QR.captcha.getOne();
if (!captcha) {
err = 'No valid captcha.';
}
}
@ -9819,8 +9855,7 @@
flag: post.flag,
textonly: textOnly,
mode: 'regist',
pwd: QR.persona.pwd,
'g-recaptcha-response': response
pwd: QR.persona.pwd
};
options = {
responseType: 'document',
@ -9832,7 +9867,7 @@
QR.cooldown.auto = false;
QR.status();
return QR.error($.el('span', {
innerHTML: "Connection error. You may have been <a href=//www.4chan.org/banned target=_blank>banned</a>.\n[<a href=\"https://github.com/MayhemYDG/4chan-x/wiki/FAQ#what-does-connection-error-you-may-have-been-banned-mean\" target=_blank>?</a>]"
innerHTML: "4chan X encountered an error while posting. [<a href=\"//4chan.org/banned\" target=\"_blank\">Banned?</a>] [<a href=\"" + E(g.FAQ) + "#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean\" target=\"_blank\">More info</a>]"
}));
}
};
@ -9851,9 +9886,33 @@
}
}
};
QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra);
QR.req.uploadStartTime = Date.now();
QR.req.progress = '...';
cb = function(response) {
if (response != null) {
extra.form.append('g-recaptcha-response', response);
}
QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra);
return QR.req.progress = '...';
};
if (typeof captcha === 'function') {
QR.req = {
progress: '...',
abort: function() {
return cb = null;
}
};
captcha(function(response) {
if (response) {
return typeof cb === "function" ? cb(response) : void 0;
} else {
delete QR.req;
post.unlock();
QR.cooldown.auto = !!QR.captcha.captchas.length;
return QR.status();
}
});
} else {
cb(captcha);
}
return QR.status();
},
response: function() {
@ -9865,8 +9924,10 @@
resDoc = req.response;
if (ban = $('.banType', resDoc)) {
board = $('.board', resDoc).innerHTML;
err = $.el('span', {
innerHTML: ban.textContent.toLowerCase() === 'banned' ? "You are banned on " + board + "! ;_;<br>\nClick <a href=//www.4chan.org/banned target=_blank>here</a> to see the reason." : "You were issued a warning on " + board + " as " + ($('.nameBlock', resDoc).innerHTML) + ".<br>\nReason: " + ($('.reason', resDoc).innerHTML)
err = $.el('span', ban.textContent.toLowerCase() === 'banned' ? {
innerHTML: "You are banned on " + $(".board", resDoc).innerHTML + "! ;_;<br>Click <a href=\"//www.4chan.org/banned\" target=\"_blank\">here</a> to see the reason."
} : {
innerHTML: "You were issued a warning on " + $(".board", resDoc).innerHTML + " as " + $(".nameBlock", resDoc).innerHTML + ".<br>Reason: " + $(".reason", resDoc).innerHTML
});
} else if (err = resDoc.getElementById('errmsg')) {
if ((_ref = $('a', err)) != null) {
@ -9885,14 +9946,11 @@
err = 'This CAPTCHA is no longer valid because it has expired.';
}
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false;
QR.cooldown.set({
delay: 2
});
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) {
QR.cooldown.addDelay(post, 2);
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i)) && !/duplicate/i.test(err.textContent)) {
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true;
QR.cooldown.set({
delay: m[1]
});
QR.cooldown.addDelay(post, +m[1]);
QR.captcha.setup(d.activeElement === QR.nodes.status);
} else {
QR.cooldown.auto = false;
}
@ -9936,7 +9994,8 @@
});
notif.onclick = function() {
QR.open();
return window.focus();
window.focus();
return QR.captcha.setup(true);
};
notif.onshow = function() {
return setTimeout(function() {
@ -9944,21 +10003,16 @@
}, 7 * $.SECOND);
};
}
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
if (!(Conf['Persistent QR'] || postsCount)) {
QR.close();
} else {
post.rm();
QR.captcha.setup(true);
QR.captcha.setup(d.activeElement === QR.nodes.status);
}
QR.cooldown.set({
req: req,
post: post,
isReply: isReply,
threadID: threadID
});
URL = threadID === postID ? Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? Build.path(g.BOARD.ID, threadID, postID) : void 0;
QR.cooldown.add(req.uploadEndTime, threadID, postID);
URL = threadID === postID ? window.location.origin + Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? window.location.origin + Build.path(g.BOARD.ID, threadID, postID) : void 0;
if (URL) {
if (Conf['Open Post in New Tab']) {
if (Conf['Open Post in New Tab'] || postsCount) {
$.open(URL);
} else {
window.location = URL;
@ -9998,226 +10052,6 @@
}
};
QR.captcha = {
init: function() {
var counter, root;
if (d.cookie.indexOf('pass_enabled=1') >= 0) {
return;
}
if (!(this.isEnabled = !!$.id('g-recaptcha'))) {
return;
}
this.captchas = [];
$.get('captchas', [], function(_arg) {
var captchas;
captchas = _arg.captchas;
return QR.captcha.sync(captchas);
});
$.sync('captchas', this.sync.bind(this));
root = $.el('div', {
className: 'captcha-root'
});
$.extend(root, {
innerHTML: "<div class=\"captcha-counter\"><a href=\"javascript:;\"></a></div>"
});
counter = $('.captcha-counter > a', root);
this.nodes = {
root: root,
counter: counter
};
this.count();
$.addClass(QR.nodes.el, 'has-captcha');
$.after(QR.nodes.com.parentNode, root);
$.on(counter, 'click', this.toggle.bind(this));
return $.on(window, 'captcha:success', (function(_this) {
return function() {
return $.queueTask(function() {
return _this.save(false);
});
};
})(this));
},
shouldFocus: false,
timeouts: {},
postsCount: 0,
needed: function() {
var captchaCount;
captchaCount = this.captchas.length;
if (this.nodes.container && !this.timeouts.destroy) {
captchaCount++;
}
this.postsCount = QR.posts.length;
if (this.postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) {
this.postsCount = 0;
}
return captchaCount < this.postsCount;
},
onPostChange: function() {
if (this.postsCount === 0) {
this.setup();
}
if (QR.posts.length === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) {
return this.postsCount = 0;
}
},
toggle: function() {
if (this.nodes.container && !this.timeouts.destroy) {
return this.destroy();
} else {
return this.setup(true, true);
}
},
setup: function(focus, force) {
if (!(this.isEnabled && (this.needed() || force))) {
return;
}
$.addClass(QR.nodes.el, 'captcha-open');
if (focus) {
this.shouldFocus = true;
}
if (this.timeouts.destroy) {
clearTimeout(this.timeouts.destroy);
delete this.timeouts.destroy;
return this.reload();
}
if (this.nodes.container) {
return;
}
this.nodes.container = $.el('div', {
className: 'captcha-container'
});
$.prepend(this.nodes.root, this.nodes.container);
new MutationObserver(this.afterSetup.bind(this)).observe(this.nodes.container, {
childList: true,
subtree: true
});
return $.globalEval('(function() {\n function render() {\n var container = document.querySelector("#qr .captcha-container");\n container.dataset.widgetID = window.grecaptcha.render(container, {\n sitekey: \'6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\',\n theme: document.documentElement.classList.contains(\'tomorrow\') ? \'dark\' : \'light\',\n callback: function(response) {\n window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));\n }\n });\n }\n if (window.grecaptcha) {\n render();\n } else {\n var cbNative = window.onRecaptchaLoaded;\n window.onRecaptchaLoaded = function() {\n render();\n cbNative();\n }\n }\n})();');
},
afterSetup: function(mutations) {
var iframe, mutation, node, textarea, _i, _j, _len, _len1, _ref;
for (_i = 0, _len = mutations.length; _i < _len; _i++) {
mutation = mutations[_i];
_ref = mutation.addedNodes;
for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
node = _ref[_j];
if (iframe = $.x('./descendant-or-self::iframe', node)) {
this.setupIFrame(iframe);
}
if (textarea = $.x('./descendant-or-self::textarea', node)) {
this.setupTextArea(textarea);
}
}
}
},
setupIFrame: function(iframe) {
this.setupTime = Date.now();
if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) {
QR.nodes.el.style.top = null;
QR.nodes.el.style.bottom = '0px';
}
if (this.shouldFocus) {
iframe.focus();
}
return this.shouldFocus = false;
},
setupTextArea: function(textarea) {
return $.one(textarea, 'input', (function(_this) {
return function() {
return _this.save(true);
};
})(this));
},
destroy: function() {
if (!this.isEnabled) {
return;
}
delete this.timeouts.destroy;
$.rmClass(QR.nodes.el, 'captcha-open');
if (this.nodes.container) {
$.rm(this.nodes.container);
}
return delete this.nodes.container;
},
sync: function(captchas) {
if (captchas == null) {
captchas = [];
}
this.captchas = captchas;
this.clear();
return this.count();
},
getOne: function() {
var captcha;
this.clear();
if (captcha = this.captchas.shift()) {
this.count();
$.set('captchas', this.captchas);
return captcha.response;
} else {
return null;
}
},
save: function(pasted) {
var reload, _base;
$.forceSync('captchas');
reload = (QR.cooldown.auto || Conf['Post on Captcha Completion']) && this.needed();
this.captchas.push({
response: $('textarea', this.nodes.container).value,
timeout: (pasted ? this.setupTime : Date.now()) + 2 * $.MINUTE
});
this.count();
$.set('captchas', this.captchas);
if (reload) {
this.shouldFocus = true;
this.reload();
} else {
if (pasted) {
this.destroy();
} else {
if ((_base = this.timeouts).destroy == null) {
_base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
}
}
QR.nodes.status.focus();
}
if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) {
return QR.submit();
}
},
clear: function() {
var captcha, i, now, _i, _len, _ref;
if (!this.captchas.length) {
return;
}
$.forceSync('captchas');
now = Date.now();
_ref = this.captchas;
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
captcha = _ref[i];
if (captcha.timeout > now) {
break;
}
}
if (!i) {
return;
}
this.captchas = this.captchas.slice(i);
this.count();
$.set('captchas', this.captchas);
return this.setup(true);
},
count: function() {
this.nodes.counter.textContent = "Captchas: " + this.captchas.length;
clearTimeout(this.timeouts.clear);
if (this.captchas.length) {
return this.timeouts.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now());
}
},
reload: function(focus) {
return $.globalEval('(function() {\n var container = document.querySelector("#qr .captcha-container");\n window.grecaptcha.reset(container.dataset.widgetID);\n})();');
}
};
QR.cooldown = {
init: function() {
var key, setTimers, type;
@ -18351,7 +18185,6 @@
}
},
updateContext: function(view) {
g.DEAD = false;
if (view === 'thread') {
g.THREADID = +window.location.pathname.split('/')[3];
}

View File

@ -9146,10 +9146,19 @@
QR = {
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'],
init: function() {
var con, sc;
var con, noscript, sc;
this.db = new DataBoard('yourPosts');
this.posts = [];
if (g.VIEW === 'archive') {
return;
}
$.globalEval('document.documentElement.dataset.jsEnabled = true;');
noscript = Conf['Force Noscript Captcha'] || !doc.dataset.jsEnabled;
this.captcha = Captcha[noscript ? 'noscript' : 'v2'];
$.on(d, '4chanXInitFinished', this.initReady);
window.addEventListener('focus', this.focus, true);
window.addEventListener('blur', this.focus, true);
$.on(d, 'click', this.focus);
Post.callbacks.push({
name: 'Quick Reply',
cb: this.node
@ -9209,12 +9218,17 @@
return;
}
QR.open();
if (Conf['Auto-Hide QR']) {
if (Conf['Auto Hide QR']) {
return QR.hide();
}
},
statusCheck: function() {
if (g.DEAD) {
var thread;
if (!QR.nodes) {
return;
}
thread = QR.posts[0].thread;
if (thread !== 'new' && g.threads["" + g.BOARD + "." + thread].isDead) {
return QR.abort();
} else {
return QR.status();
@ -9242,9 +9256,11 @@
open: function() {
var err;
if (QR.nodes) {
if (QR.nodes.el.hidden) {
QR.captcha.setup();
}
QR.nodes.el.hidden = false;
QR.unhide();
QR.captcha.setup();
return;
}
try {
@ -9281,14 +9297,34 @@
QR.status();
return QR.captcha.destroy();
},
focusin: function() {
if ($.hasClass(QR.nodes.el, 'autohide') && !$.hasClass(QR.nodes.el, 'focus')) {
QR.captcha.setup();
}
return $.addClass(QR.nodes.el, 'focus');
focus: function() {
return $.queueTask(function() {
var focus;
if (!QR.nodes) {
return;
}
if (!$$('.goog-bubble-content > iframe').some(function(el) {
return el.getBoundingClientRect().top >= 0;
})) {
focus = d.activeElement && QR.nodes.el.contains(d.activeElement);
$[focus ? 'addClass' : 'rmClass'](QR.nodes.el, 'focus');
}
if (typeof chrome !== "undefined" && chrome !== null) {
if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') {
QR.scrollY = window.scrollY;
return $.on(d, 'scroll', QR.scrollLock);
} else {
return $.off(d, 'scroll', QR.scrollLock);
}
}
});
},
focusout: function() {
return $.rmClass(QR.nodes.el, 'focus');
scrollLock: function(e) {
if (d.activeElement && QR.nodes.el.contains(d.activeElement) && d.activeElement.nodeName === 'IFRAME') {
return window.scroll(window.scrollX, QR.scrollY);
} else {
return $.off(d, 'scroll', QR.scrollLock);
}
},
hide: function() {
d.activeElement.blur();
@ -9317,8 +9353,10 @@
}
if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
QR.captcha.setup(true);
QR.captcha.notify(el);
} else {
QR.notify(el);
}
QR.notify(el);
if (d.hidden) {
return alert(el.textContent);
}
@ -9383,7 +9421,7 @@
}
sel = d.getSelection();
post = Get.postFromNode(this);
text = ">>" + post + "\n";
text = post.board.ID === g.BOARD.ID ? ">>" + post + "\n" : ">>>/" + post.board + "/" + post + "\n";
if (sel.toString().trim() && post === Get.postFromNode(sel.anchorNode)) {
range = sel.getRangeAt(0);
frag = range.cloneContents();
@ -9612,16 +9650,17 @@
}
list.value = g.VIEW === 'thread' ? g.THREADID : 'new';
if ($.hasClass(list, 'riced')) {
return list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent;
list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent;
}
return (g.VIEW === 'thread' ? $.addClass : $.rmClass)(QR.nodes.el, 'reply-to-thread');
},
dialog: function() {
var dialog, elm, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode;
var dialog, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode;
QR.nodes = nodes = {
el: dialog = UI.dialog('qr', 'top:0;right:0;')
};
$.extend(dialog, {
innerHTML: "<div id=qrtab class=move>\r<input type=checkbox id=autohide title=Auto-hide>\r<div id=qr-thread-select>\r<select data-name=thread title='Create a new thread / Reply'>\r<option value=new>New thread</option>\r</select>\r</div>\r<a href=javascript:; class='close fa' title=Close>\\uf00d</a>\r</div>\r<form>\r<div class=persona>\r<input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1>\r<input name=email data-name=email list=\"list-email\" placeholder=Options class=field size=1>\r<input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1> \r</div>\r<div class=textarea>\r<textarea data-name=com placeholder=Comment class=field></textarea>\r<span id=char-count></span>\r</div>\r<div id=dump-list-container>\r<div id=dump-list></div>\r<a id=add-post href=javascript:; title=\"Add a post\">+</a>\r</div>\r<div id=file-n-submit>\r<span id=qr-filename-container class=field tabindex=0>\r<span id=qr-no-file>No selected file</span>\r<input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\">\r<span id=qr-extras-container>\r<label id=qr-spoiler-label>\r<input type=checkbox id=qr-file-spoiler title='Spoiler image'>\r</label>\r<span class=description>Spoiler</span>\r<a id=url-button><i class=\"fa\">\\uf0c1</i></a>\r<span class=description>Post from URL</span>\r<a id=dump-button title='Dump list'>+</a>\r<span class=description>Dump</span>\r<a id=qr-filerm href=javascript:; title='Remove file' class=fa>\\uf00d</a>\r<span class=description>Remove File</span>\r</span>\r</span>\r<input type=submit>\r</div>\r<input type=file multiple>\r</form>\r<datalist id=\"list-name\"></datalist>\r<datalist id=\"list-email\"></datalist>\r<datalist id=\"list-sub\"></datalist>\r"
innerHTML: "<div id=qrtab class=move>\r<input type=checkbox id=autohide title=Auto-hide>\r<div id=qr-thread-select>\r<select data-name=thread title='Create a new thread / Reply'>\r<option value=new>New thread</option>\r</select>\r</div>\r<a href=javascript:; class='close fa' title=Close>\\uf00d</a>\r</div>\r<form>\r<div class=persona>\r<input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1>\r<input name=email data-name=email list=\"list-email\" placeholder=Options class=field size=1>\r<input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1> \r</div>\r<div class=textarea>\r<textarea data-name=com placeholder=Comment class=field></textarea>\r<span id=char-count></span>\r</div>\r<div id=dump-list-container>\r<div id=dump-list></div>\r<a id=add-post href=javascript:; title=\"Add a post\">+</a>\r</div>\r<div id=file-n-submit>\r<span id=qr-filename-container class=field tabindex=0>\r<span id=qr-no-file>No selected file</span>\r<input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\">\r<span id=qr-extras-container>\r<label id=qr-spoiler-label>\r<input type=checkbox id=qr-file-spoiler title='Spoiler image'>\r</label>\r<span class=description>Spoiler</span>\r<a id=url-button><i class=\"fa\">\\uf0c1</i></a>\r<span class=description>Post from URL</span>\r<a id=dump-button title='Dump list'>+</a>\r<span class=description>Dump</span>\r<a id=qr-filerm href=javascript:; title='Remove file' class=fa>\\uf00d</a>\r<span class=description>Remove File</span>\r</span>\r</span>\r<input type=submit>\r</div>\r<input type=file multiple>\r</form>\r<datalist id=\"list-name\"></datalist>\r<datalist id=\"list-email\"></datalist>\r<datalist id=\"list-sub\"></datalist>\r"
});
setNode = function(name, query) {
return nodes[name] = $(query, dialog);
@ -9661,6 +9700,14 @@
QR.max_size_video = 3145728;
QR.max_width_video = QR.max_height_video = 2048;
QR.max_duration_video = 120;
if (Conf['Show New Thread Option in Threads']) {
$.addClass(QR.nodes.el, 'show-new-thread-option');
}
if (Conf['Show Name and Subject']) {
$.addClass(QR.nodes.name, 'force-show');
$.addClass(QR.nodes.sub, 'force-show');
QR.nodes.email.placeholder = 'E-mail';
}
QR.forcedAnon = !!$('form[name="post"] input[name="name"][type="hidden"]');
if (QR.forcedAnon) {
$.addClass(QR.nodes.el, 'forced-anon');
@ -9677,20 +9724,15 @@
}
if (g.BOARD.ID === 'f' && g.VIEW !== 'thread') {
nodes.flashTag = $.el('select', {
name: 'filetag',
innerHTML: "<option value=0>Hentai</option>\n<option value=6>Porn</option>\n<option value=1>Japanese</option>\n<option value=2>Anime</option>\n<option value=3>Game</option>\n<option value=5>Loop</option>\n<option value=4 selected>Other</option>"
});
name: 'filetag'
}, $.extend(nodes.flashTag, {
innerHTML: "<option value=\"0\">Hentai</option><option value=\"6\">Porn</option><option value=\"1\">Japanese</option><option value=\"2\">Anime</option><option value=\"3\">Game</option><option value=\"5\">Loop</option><option value=\"4\" selected>Other</option>"
}));
nodes.flashTag.dataset["default"] = '4';
$.add(nodes.form, nodes.flashTag);
}
QR.flagsInput();
$.on(nodes.filename.parentNode, 'click keydown', QR.openFileInput);
items = $$('*', QR.nodes.el);
i = 0;
while (elm = items[i++]) {
$.on(elm, 'blur', QR.focusout);
$.on(elm, 'focus', QR.focusin);
}
$.on(nodes.autohide, 'change', QR.toggleHide);
$.on(nodes.close, 'click', QR.close);
$.on(nodes.dumpButton, 'click', function() {
@ -9719,7 +9761,7 @@
while (name = items[i++]) {
$.on(nodes[name], 'mouseover', QR.mouseover);
}
items = ['name', 'email', 'sub', 'com', 'filename', 'flag'];
items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag'];
i = 0;
save = function() {
return QR.selected.save(this);
@ -9778,25 +9820,18 @@
return select;
},
flagsInput: function() {
var flag, nodes;
var nodes;
nodes = QR.nodes;
if (!nodes) {
return;
}
if (nodes.flag) {
$.rm(nodes.flag);
delete nodes.flag;
}
if (g.BOARD.ID === 'pol') {
flag = QR.flags();
flag.dataset.name = 'flag';
flag.dataset["default"] = '0';
nodes.flag = flag;
return $.add(nodes.form, flag);
return delete nodes.flag;
}
},
submit: function(e) {
var err, extra, filetag, formData, options, post, response, textOnly, thread, threadID;
var captcha, cb, err, extra, filetag, formData, options, post, textOnly, thread, threadID;
if (e != null) {
e.preventDefault();
}
@ -9831,8 +9866,8 @@
err = 'Max limit of image replies has been reached.';
}
if (QR.captcha.isEnabled && !err) {
response = QR.captcha.getOne();
if (!response) {
captcha = QR.captcha.getOne();
if (!captcha) {
err = 'No valid captcha.';
}
}
@ -9863,8 +9898,7 @@
flag: post.flag,
textonly: textOnly,
mode: 'regist',
pwd: QR.persona.pwd,
'g-recaptcha-response': response
pwd: QR.persona.pwd
};
options = {
responseType: 'document',
@ -9876,7 +9910,7 @@
QR.cooldown.auto = false;
QR.status();
return QR.error($.el('span', {
innerHTML: "Connection error. You may have been <a href=//www.4chan.org/banned target=_blank>banned</a>.\n[<a href=\"https://github.com/MayhemYDG/4chan-x/wiki/FAQ#what-does-connection-error-you-may-have-been-banned-mean\" target=_blank>?</a>]"
innerHTML: "4chan X encountered an error while posting. [<a href=\"//4chan.org/banned\" target=\"_blank\">Banned?</a>] [<a href=\"" + E(g.FAQ) + "#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean\" target=\"_blank\">More info</a>]"
}));
}
};
@ -9895,9 +9929,33 @@
}
}
};
QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra);
QR.req.uploadStartTime = Date.now();
QR.req.progress = '...';
cb = function(response) {
if (response != null) {
extra.form.append('g-recaptcha-response', response);
}
QR.req = $.ajax("https://sys.4chan.org/" + g.BOARD + "/post", options, extra);
return QR.req.progress = '...';
};
if (typeof captcha === 'function') {
QR.req = {
progress: '...',
abort: function() {
return cb = null;
}
};
captcha(function(response) {
if (response) {
return typeof cb === "function" ? cb(response) : void 0;
} else {
delete QR.req;
post.unlock();
QR.cooldown.auto = !!QR.captcha.captchas.length;
return QR.status();
}
});
} else {
cb(captcha);
}
return QR.status();
},
response: function() {
@ -9909,8 +9967,10 @@
resDoc = req.response;
if (ban = $('.banType', resDoc)) {
board = $('.board', resDoc).innerHTML;
err = $.el('span', {
innerHTML: ban.textContent.toLowerCase() === 'banned' ? "You are banned on " + board + "! ;_;<br>\nClick <a href=//www.4chan.org/banned target=_blank>here</a> to see the reason." : "You were issued a warning on " + board + " as " + ($('.nameBlock', resDoc).innerHTML) + ".<br>\nReason: " + ($('.reason', resDoc).innerHTML)
err = $.el('span', ban.textContent.toLowerCase() === 'banned' ? {
innerHTML: "You are banned on " + $(".board", resDoc).innerHTML + "! ;_;<br>Click <a href=\"//www.4chan.org/banned\" target=\"_blank\">here</a> to see the reason."
} : {
innerHTML: "You were issued a warning on " + $(".board", resDoc).innerHTML + " as " + $(".nameBlock", resDoc).innerHTML + ".<br>Reason: " + $(".reason", resDoc).innerHTML
});
} else if (err = resDoc.getElementById('errmsg')) {
if ((_ref = $('a', err)) != null) {
@ -9929,14 +9989,11 @@
err = 'This CAPTCHA is no longer valid because it has expired.';
}
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false;
QR.cooldown.set({
delay: 2
});
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) {
QR.cooldown.addDelay(post, 2);
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i)) && !/duplicate/i.test(err.textContent)) {
QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true;
QR.cooldown.set({
delay: m[1]
});
QR.cooldown.addDelay(post, +m[1]);
QR.captcha.setup(d.activeElement === QR.nodes.status);
} else {
QR.cooldown.auto = false;
}
@ -9980,7 +10037,8 @@
});
notif.onclick = function() {
QR.open();
return window.focus();
window.focus();
return QR.captcha.setup(true);
};
notif.onshow = function() {
return setTimeout(function() {
@ -9988,21 +10046,16 @@
}, 7 * $.SECOND);
};
}
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
if (!(Conf['Persistent QR'] || postsCount)) {
QR.close();
} else {
post.rm();
QR.captcha.setup(true);
QR.captcha.setup(d.activeElement === QR.nodes.status);
}
QR.cooldown.set({
req: req,
post: post,
isReply: isReply,
threadID: threadID
});
URL = threadID === postID ? Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? Build.path(g.BOARD.ID, threadID, postID) : void 0;
QR.cooldown.add(req.uploadEndTime, threadID, postID);
URL = threadID === postID ? window.location.origin + Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? window.location.origin + Build.path(g.BOARD.ID, threadID, postID) : void 0;
if (URL) {
if (Conf['Open Post in New Tab']) {
if (Conf['Open Post in New Tab'] || postsCount) {
$.open(URL);
} else {
window.location = URL;
@ -10042,226 +10095,6 @@
}
};
QR.captcha = {
init: function() {
var counter, root;
if (d.cookie.indexOf('pass_enabled=1') >= 0) {
return;
}
if (!(this.isEnabled = !!$.id('g-recaptcha'))) {
return;
}
this.captchas = [];
$.get('captchas', [], function(_arg) {
var captchas;
captchas = _arg.captchas;
return QR.captcha.sync(captchas);
});
$.sync('captchas', this.sync.bind(this));
root = $.el('div', {
className: 'captcha-root'
});
$.extend(root, {
innerHTML: "<div class=\"captcha-counter\"><a href=\"javascript:;\"></a></div>"
});
counter = $('.captcha-counter > a', root);
this.nodes = {
root: root,
counter: counter
};
this.count();
$.addClass(QR.nodes.el, 'has-captcha');
$.after(QR.nodes.com.parentNode, root);
$.on(counter, 'click', this.toggle.bind(this));
return $.on(window, 'captcha:success', (function(_this) {
return function() {
return $.queueTask(function() {
return _this.save(false);
});
};
})(this));
},
shouldFocus: false,
timeouts: {},
postsCount: 0,
needed: function() {
var captchaCount;
captchaCount = this.captchas.length;
if (this.nodes.container && !this.timeouts.destroy) {
captchaCount++;
}
this.postsCount = QR.posts.length;
if (this.postsCount === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) {
this.postsCount = 0;
}
return captchaCount < this.postsCount;
},
onPostChange: function() {
if (this.postsCount === 0) {
this.setup();
}
if (QR.posts.length === 1 && !Conf['Auto-load captcha'] && !QR.posts[0].com && !QR.posts[0].file) {
return this.postsCount = 0;
}
},
toggle: function() {
if (this.nodes.container && !this.timeouts.destroy) {
return this.destroy();
} else {
return this.setup(true, true);
}
},
setup: function(focus, force) {
if (!(this.isEnabled && (this.needed() || force))) {
return;
}
$.addClass(QR.nodes.el, 'captcha-open');
if (focus) {
this.shouldFocus = true;
}
if (this.timeouts.destroy) {
clearTimeout(this.timeouts.destroy);
delete this.timeouts.destroy;
return this.reload();
}
if (this.nodes.container) {
return;
}
this.nodes.container = $.el('div', {
className: 'captcha-container'
});
$.prepend(this.nodes.root, this.nodes.container);
new MutationObserver(this.afterSetup.bind(this)).observe(this.nodes.container, {
childList: true,
subtree: true
});
return $.globalEval('(function() {\n function render() {\n var container = document.querySelector("#qr .captcha-container");\n container.dataset.widgetID = window.grecaptcha.render(container, {\n sitekey: \'6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\',\n theme: document.documentElement.classList.contains(\'tomorrow\') ? \'dark\' : \'light\',\n callback: function(response) {\n window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));\n }\n });\n }\n if (window.grecaptcha) {\n render();\n } else {\n var cbNative = window.onRecaptchaLoaded;\n window.onRecaptchaLoaded = function() {\n render();\n cbNative();\n }\n }\n})();');
},
afterSetup: function(mutations) {
var iframe, mutation, node, textarea, _i, _j, _len, _len1, _ref;
for (_i = 0, _len = mutations.length; _i < _len; _i++) {
mutation = mutations[_i];
_ref = mutation.addedNodes;
for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
node = _ref[_j];
if (iframe = $.x('./descendant-or-self::iframe', node)) {
this.setupIFrame(iframe);
}
if (textarea = $.x('./descendant-or-self::textarea', node)) {
this.setupTextArea(textarea);
}
}
}
},
setupIFrame: function(iframe) {
this.setupTime = Date.now();
if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) {
QR.nodes.el.style.top = null;
QR.nodes.el.style.bottom = '0px';
}
if (this.shouldFocus) {
iframe.focus();
}
return this.shouldFocus = false;
},
setupTextArea: function(textarea) {
return $.one(textarea, 'input', (function(_this) {
return function() {
return _this.save(true);
};
})(this));
},
destroy: function() {
if (!this.isEnabled) {
return;
}
delete this.timeouts.destroy;
$.rmClass(QR.nodes.el, 'captcha-open');
if (this.nodes.container) {
$.rm(this.nodes.container);
}
return delete this.nodes.container;
},
sync: function(captchas) {
if (captchas == null) {
captchas = [];
}
this.captchas = captchas;
this.clear();
return this.count();
},
getOne: function() {
var captcha;
this.clear();
if (captcha = this.captchas.shift()) {
this.count();
$.set('captchas', this.captchas);
return captcha.response;
} else {
return null;
}
},
save: function(pasted) {
var reload, _base;
$.forceSync('captchas');
reload = (QR.cooldown.auto || Conf['Post on Captcha Completion']) && this.needed();
this.captchas.push({
response: $('textarea', this.nodes.container).value,
timeout: (pasted ? this.setupTime : Date.now()) + 2 * $.MINUTE
});
this.count();
$.set('captchas', this.captchas);
if (reload) {
this.shouldFocus = true;
this.reload();
} else {
if (pasted) {
this.destroy();
} else {
if ((_base = this.timeouts).destroy == null) {
_base.destroy = setTimeout(this.destroy.bind(this), 3 * $.SECOND);
}
}
QR.nodes.status.focus();
}
if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) {
return QR.submit();
}
},
clear: function() {
var captcha, i, now, _i, _len, _ref;
if (!this.captchas.length) {
return;
}
$.forceSync('captchas');
now = Date.now();
_ref = this.captchas;
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
captcha = _ref[i];
if (captcha.timeout > now) {
break;
}
}
if (!i) {
return;
}
this.captchas = this.captchas.slice(i);
this.count();
$.set('captchas', this.captchas);
return this.setup(true);
},
count: function() {
this.nodes.counter.textContent = "Captchas: " + this.captchas.length;
clearTimeout(this.timeouts.clear);
if (this.captchas.length) {
return this.timeouts.clear = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now());
}
},
reload: function(focus) {
return $.globalEval('(function() {\n var container = document.querySelector("#qr .captcha-container");\n window.grecaptcha.reset(container.dataset.widgetID);\n})();');
}
};
QR.cooldown = {
init: function() {
var key, setTimers, type;
@ -18383,7 +18216,6 @@
}
},
updateContext: function(view) {
g.DEAD = false;
if (view === 'thread') {
g.THREADID = +window.location.pathname.split('/')[3];
}

View File

@ -104,7 +104,6 @@ Navigate =
updateContext: (view) ->
# State tracking
g.DEAD = false
g.THREADID = +window.location.pathname.split('/')[3] if view is 'thread'
{

View File

@ -9,9 +9,9 @@
</div>
<form>
<div class=persona>
<input name=name data-name=name list="list-name" placeholder=Name class=field size=1>
<input name=name data-name=name list="list-name" placeholder=Name class=field size=1>
<input name=email data-name=email list="list-email" placeholder=Options class=field size=1>
<input name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1>
<input name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1>
</div>
<div class=textarea>
<textarea data-name=com placeholder=Comment class=field></textarea>

View File

@ -1,173 +0,0 @@
QR.captcha =
init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$.id 'g-recaptcha'
@captchas = []
$.get 'captchas', [], ({captchas}) ->
QR.captcha.sync captchas
$.sync 'captchas', @sync.bind @
root = $.el 'div', className: 'captcha-root'
$.extend root, <%= html(
'<div class="captcha-counter"><a href="javascript:;"></a></div>'
) %>
counter = $ '.captcha-counter > a', root
@nodes = {root, counter}
@count()
$.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.com.parentNode, root
$.on counter, 'click', @toggle.bind @
$.on window, 'captcha:success', =>
# XXX Greasemonkey 1.x workaround to gain access to GM_* functions.
$.queueTask => @save false
shouldFocus: false
timeouts: {}
postsCount: 0
needed: ->
captchaCount = @captchas.length
captchaCount++ if @nodes.container and !@timeouts.destroy
@postsCount = QR.posts.length
@postsCount = 0 if @postsCount is 1 and !Conf['Auto-load captcha'] and !QR.posts[0].com and !QR.posts[0].file
captchaCount < @postsCount
onPostChange: ->
@setup() if @postsCount is 0
@postsCount = 0 if QR.posts.length is 1 and !Conf['Auto-load captcha'] and !QR.posts[0].com and !QR.posts[0].file
toggle: ->
if @nodes.container and !@timeouts.destroy
@destroy()
else
@setup true, true
setup: (focus, force) ->
return unless @isEnabled and (@needed() or force)
$.addClass QR.nodes.el, 'captcha-open'
@shouldFocus = true if focus
if @timeouts.destroy
clearTimeout @timeouts.destroy
delete @timeouts.destroy
return @reload()
return if @nodes.container
@nodes.container = $.el 'div', className: 'captcha-container'
$.prepend @nodes.root, @nodes.container
new MutationObserver(@afterSetup.bind @).observe @nodes.container,
childList: true
subtree: true
$.globalEval '''
(function() {
function render() {
var container = document.querySelector("#qr .captcha-container");
container.dataset.widgetID = window.grecaptcha.render(container, {
sitekey: '<%= meta.recaptchaKey %>',
theme: document.documentElement.classList.contains('tomorrow') ? 'dark' : 'light',
callback: function(response) {
window.dispatchEvent(new CustomEvent("captcha:success", {detail: response}));
}
});
}
if (window.grecaptcha) {
render();
} else {
var cbNative = window.onRecaptchaLoaded;
window.onRecaptchaLoaded = function() {
render();
cbNative();
}
}
})();
'''
afterSetup: (mutations) ->
for mutation in mutations
for node in mutation.addedNodes
@setupIFrame iframe if iframe = $.x './descendant-or-self::iframe', node
@setupTextArea textarea if textarea = $.x './descendant-or-self::textarea', node
return
setupIFrame: (iframe) ->
@setupTime = Date.now()
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null
QR.nodes.el.style.bottom = '0px'
iframe.focus() if @shouldFocus
@shouldFocus = false
setupTextArea: (textarea) ->
$.one textarea, 'input', => @save true
destroy: ->
return unless @isEnabled
delete @timeouts.destroy
$.rmClass QR.nodes.el, 'captcha-open'
$.rm @nodes.container if @nodes.container
delete @nodes.container
sync: (captchas=[]) ->
@captchas = captchas
@clear()
@count()
getOne: ->
@clear()
if captcha = @captchas.shift()
@count()
$.set 'captchas', @captchas
captcha.response
else
null
save: (pasted) ->
$.forceSync 'captchas'
reload = (QR.cooldown.auto or Conf['Post on Captcha Completion']) and @needed()
@captchas.push
response: $('textarea', @nodes.container).value
timeout: (if pasted then @setupTime else Date.now()) + 2 * $.MINUTE
@count()
$.set 'captchas', @captchas
if reload
@shouldFocus = true
@reload()
else
if pasted
@destroy()
else
@timeouts.destroy ?= setTimeout @destroy.bind(@), 3 * $.SECOND
QR.nodes.status.focus()
QR.submit() if Conf['Post on Captcha Completion'] and !QR.cooldown.auto
clear: ->
return unless @captchas.length
$.forceSync 'captchas'
now = Date.now()
for captcha, i in @captchas
break if captcha.timeout > now
return unless i
@captchas = @captchas[i..]
@count()
$.set 'captchas', @captchas
@setup true
count: ->
@nodes.counter.textContent = "Captchas: #{@captchas.length}"
clearTimeout @timeouts.clear
if @captchas.length
@timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
reload: (focus) ->
$.globalEval '''
(function() {
var container = document.querySelector("#qr .captcha-container");
window.grecaptcha.reset(container.dataset.widgetID);
})();
'''

View File

@ -5,8 +5,19 @@ QR =
@db = new DataBoard 'yourPosts'
@posts = []
return if g.VIEW is 'archive'
$.globalEval 'document.documentElement.dataset.jsEnabled = true;'
noscript = Conf['Force Noscript Captcha'] or !doc.dataset.jsEnabled
@captcha = Captcha[if noscript then 'noscript' else 'v2']
$.on d, '4chanXInitFinished', @initReady
window.addEventListener 'focus', @focus, true
window.addEventListener 'blur', @focus, true
# We don't receive blur events from captcha iframe.
$.on d, 'click', @focus
Post.callbacks.push
name: 'Quick Reply'
cb: @node
@ -45,9 +56,7 @@ QR =
QR.postingIsEnabled = !!$.id 'postForm'
return unless QR.postingIsEnabled
<% if (type === 'crx') { %>
$.on d, 'paste', QR.paste
<% } %>
$.on d, 'dragover', QR.dragOver
$.on d, 'drop', QR.dropFile
$.on d, 'dragstart dragend', QR.drag
@ -58,10 +67,12 @@ QR =
return if !Conf['Persistent QR']
QR.open()
QR.hide() if Conf['Auto-Hide QR']
QR.hide() if Conf['Auto Hide QR']
statusCheck: ->
if g.DEAD
return unless QR.nodes
{thread} = QR.posts[0]
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead
QR.abort()
else
QR.status()
@ -78,9 +89,9 @@ QR =
open: ->
if QR.nodes
QR.captcha.setup() if QR.nodes.el.hidden
QR.nodes.el.hidden = false
QR.unhide()
QR.captcha.setup()
return
try
QR.dialog()
@ -107,12 +118,25 @@ QR =
QR.status()
QR.captcha.destroy()
focusin: ->
QR.captcha.setup() if $.hasClass(QR.nodes.el, 'autohide') and !$.hasClass(QR.nodes.el, 'focus')
$.addClass QR.nodes.el, 'focus'
focus: ->
$.queueTask ->
return unless QR.nodes
unless $$('.goog-bubble-content > iframe').some((el) -> el.getBoundingClientRect().top >= 0)
focus = d.activeElement and QR.nodes.el.contains(d.activeElement)
$[if focus then 'addClass' else 'rmClass'] QR.nodes.el, 'focus'
if chrome?
# XXX Stop anomalous scrolling on space/tab in captcha iframe.
if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME'
QR.scrollY = window.scrollY
$.on d, 'scroll', QR.scrollLock
else
$.off d, 'scroll', QR.scrollLock
focusout: ->
$.rmClass QR.nodes.el, 'focus'
scrollLock: (e) ->
if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME'
window.scroll window.scrollX, QR.scrollY
else
$.off d, 'scroll', QR.scrollLock
hide: ->
d.activeElement.blur()
@ -138,7 +162,9 @@ QR =
el.removeAttribute 'style'
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
QR.captcha.setup true
QR.notify el
QR.captcha.notify el
else
QR.notify el
alert el.textContent if d.hidden
notify: (el) ->
@ -196,7 +222,7 @@ QR =
sel = d.getSelection()
post = Get.postFromNode @
text = ">>#{post}\n"
text = if post.board.ID is g.BOARD.ID then ">>#{post}\n" else ">>>/#{post.board}/#{post}\n"
if sel.toString().trim() and post is Get.postFromNode sel.anchorNode
range = sel.getRangeAt 0
frag = range.cloneContents()
@ -356,6 +382,7 @@ QR =
'new'
list.nextElementSibling.firstChild.textContent = list.options[list.selectedIndex].textContent if $.hasClass list, 'riced'
(if g.VIEW is 'thread' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
dialog: ->
QR.nodes = nodes =
@ -405,6 +432,14 @@ QR =
QR.max_width_video = QR.max_height_video = 2048
QR.max_duration_video = 120
if Conf['Show New Thread Option in Threads']
$.addClass QR.nodes.el, 'show-new-thread-option'
if Conf['Show Name and Subject']
$.addClass QR.nodes.name, 'force-show'
$.addClass QR.nodes.sub, 'force-show'
QR.nodes.email.placeholder = 'E-mail'
QR.forcedAnon = !!$ 'form[name="post"] input[name="name"][type="hidden"]'
if QR.forcedAnon
$.addClass QR.nodes.el, 'forced-anon'
@ -420,16 +455,16 @@ QR =
nodes.addPost.tabIndex = 35
if g.BOARD.ID is 'f' and g.VIEW isnt 'thread'
nodes.flashTag = $.el 'select', name: 'filetag',
innerHTML: """
<option value=0>Hentai</option>
<option value=6>Porn</option>
<option value=1>Japanese</option>
<option value=2>Anime</option>
<option value=3>Game</option>
<option value=5>Loop</option>
<option value=4 selected>Other</option>
"""
nodes.flashTag = $.el 'select', name: 'filetag',
$.extend nodes.flashTag, <%= html(
'<option value="0">Hentai</option>' +
'<option value="6">Porn</option>' +
'<option value="1">Japanese</option>' +
'<option value="2">Anime</option>' +
'<option value="3">Game</option>' +
'<option value="5">Loop</option>' +
'<option value="4" selected>Other</option>'
) %>
nodes.flashTag.dataset.default = '4'
$.add nodes.form, nodes.flashTag
@ -437,12 +472,6 @@ QR =
$.on nodes.filename.parentNode, 'click keydown', QR.openFileInput
items = $$ '*', QR.nodes.el
i = 0
while elm = items[i++]
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
$.on nodes.autohide, 'change', QR.toggleHide
$.on nodes.close, 'click', QR.close
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
@ -462,7 +491,7 @@ QR =
$.on nodes[name], 'mouseover', QR.mouseover
# save selected post's data
items = ['name', 'email', 'sub', 'com', 'filename', 'flag']
items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
i = 0
save = -> QR.selected.save @
while name = items[i++]
@ -549,12 +578,13 @@ QR =
$.rm nodes.flag
delete nodes.flag
if g.BOARD.ID is 'pol'
flag = QR.flags()
flag.dataset.name = 'flag'
flag.dataset.default = '0'
nodes.flag = flag
$.add nodes.form, flag
# # if false?
# if g.BOARD.ID is 'pol'
# flag = QR.flags()
# flag.dataset.name = 'flag'
# flag.dataset.default = '0'
# nodes.flag = flag
# $.add nodes.form, flag
submit: (e) ->
e?.preventDefault()
@ -590,8 +620,8 @@ QR =
err = 'Max limit of image replies has been reached.'
if QR.captcha.isEnabled and !err
response = QR.captcha.getOne()
err = 'No valid captcha.' unless response
captcha = QR.captcha.getOne()
err = 'No valid captcha.' unless captcha
QR.cleanNotifications()
if err
@ -613,10 +643,8 @@ QR =
formData =
resto: threadID
name: post.name unless QR.forcedAnon
email: post.email
sub: post.sub unless QR.forcedAnon or threadID
com: post.com
upfile: post.file
@ -626,7 +654,6 @@ QR =
textonly: textOnly
mode: 'regist'
pwd: QR.persona.pwd
'g-recaptcha-response': response
options =
responseType: 'document'
@ -639,10 +666,11 @@ QR =
QR.cooldown.auto = false
QR.status()
QR.error $.el 'span',
innerHTML: """
Connection error. You may have been <a href=//www.4chan.org/banned target=_blank>banned</a>.
[<a href="https://github.com/MayhemYDG/4chan-x/wiki/FAQ#what-does-connection-error-you-may-have-been-banned-mean" target=_blank>?</a>]
"""
<%= html(
'4chan X encountered an error while posting. ' +
'[<a href="//4chan.org/banned" target="_blank">Banned?</a>] ' +
'[<a href="${g.FAQ}#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean" target="_blank">More info</a>]'
) %>
extra =
form: $.formData formData
upCallbacks:
@ -657,11 +685,29 @@ QR =
QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
QR.status()
QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra
cb = (response) ->
extra.form.append 'g-recaptcha-response', response if response?
QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra
QR.req.progress = '...'
if typeof captcha is 'function'
# Wait for captcha to be verified before submitting post.
QR.req =
progress: '...'
abort: -> cb = null
captcha (response) ->
if response
cb? response
else
delete QR.req
post.unlock()
QR.cooldown.auto = !!QR.captcha.captchas.length
QR.status()
else
cb captcha
# Starting to upload might take some time.
# Provide some feedback that we're starting to submit.
QR.req.uploadStartTime = Date.now()
QR.req.progress = '...'
QR.status()
response: ->
@ -674,17 +720,11 @@ QR =
resDoc = req.response
if ban = $ '.banType', resDoc # banned/warning
board = $('.board', resDoc).innerHTML
err = $.el 'span', innerHTML:
err = $.el 'span',
if ban.textContent.toLowerCase() is 'banned'
"""
You are banned on #{board}! ;_;<br>
Click <a href=//www.4chan.org/banned target=_blank>here</a> to see the reason.
"""
<%= html('You are banned on &{$(".board", resDoc)}! ;_;<br>Click <a href="//www.4chan.org/banned" target="_blank">here</a> to see the reason.') %>
else
"""
You were issued a warning on #{board} as #{$('.nameBlock', resDoc).innerHTML}.<br>
Reason: #{$('.reason', resDoc).innerHTML}
"""
<%= html('You were issued a warning on &{$(".board", resDoc)} as &{$(".nameBlock", resDoc)}.<br>Reason: &{$(".reason", resDoc)}') %>
else if err = resDoc.getElementById 'errmsg' # error!
$('a', err)?.target = '_blank' # duplicate image link
else if resDoc.title isnt 'Post successful!'
@ -710,13 +750,14 @@ QR =
false
# Too many frequent mistyped captchas will auto-ban you!
# On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2
else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i
QR.cooldown.addDelay post, 2
else if err.textContent and (m = err.textContent.match /wait\s+(\d+)\s+second/i) and !/duplicate/i.test err.textContent
QR.cooldown.auto = if QR.captcha.isEnabled
!!QR.captcha.captchas.length
else
true
QR.cooldown.set delay: m[1]
QR.cooldown.addDelay post, +m[1]
QR.captcha.setup (d.activeElement is QR.nodes.status)
else # stop auto-posting
QR.cooldown.auto = false
QR.status()
@ -762,26 +803,27 @@ QR =
notif.onclick = ->
QR.open()
window.focus()
QR.captcha.setup true
notif.onshow = ->
setTimeout ->
notif.close()
, 7 * $.SECOND
unless Conf['Persistent QR'] or QR.cooldown.auto
unless Conf['Persistent QR'] or postsCount
QR.close()
else
post.rm()
QR.captcha.setup true
QR.captcha.setup(d.activeElement is QR.nodes.status)
QR.cooldown.set {req, post, isReply, threadID}
QR.cooldown.add req.uploadEndTime, threadID, postID
URL = if threadID is postID # new thread
Build.path g.BOARD.ID, threadID
window.location.origin + Build.path g.BOARD.ID, threadID
else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index
Build.path g.BOARD.ID, threadID, postID
window.location.origin + Build.path g.BOARD.ID, threadID, postID
if URL
if Conf['Open Post in New Tab']
if Conf['Open Post in New Tab'] or postsCount
$.open URL
else
window.location = URL