Merge branch 'v3'

Conflicts:
	CHANGELOG.md
	Gruntfile.coffee
	LICENSE
	builds/4chan-X.meta.js
	builds/4chan-X.user.js
	builds/crx/manifest.json
	builds/crx/script.js
	latest.js
	package.json
	src/General/css/style.css
	src/Posting/QuickReply.coffee
This commit is contained in:
Zixaphir 2013-09-24 14:46:24 -07:00
commit 7812001d96
23 changed files with 1833 additions and 582 deletions

View File

@ -1,3 +1,27 @@
**MayhemYDG**:
- Tiny posting cooldown adjustment:
* You can post an image reply immediately after a non-image reply.
- Update posting cooldown timers to match 4chan settings:
* Cooldown may vary between inter-thread and intra-thread replies.
* Cooldown may vary when posting a file or not.
* Cooldown does not take sageing into account anymore.
* Timers vary across boards.
- Updated post and deletion cooldown timers to match 4chan changes: they are now twice longer.
- Added support for the flag selector on /pol/.
**seaweedchan**:
- Fix thread updater bug introduced in last version
- Just some small fixes.
**zixaphir**:
- Some changes to mascots
* Silhouette mascots are now generated dynamically with the silhouette filter
* Images are now compressed client side before being uploaded via the upload interface (this is only for mascots, not posts)
- Fix an issue with Linkifier linkifying replaced spoilers
- Fix an issue with "fit height" in the gallery on Chrome
- Small thread updater fixes
### v2.3.10 ### v2.3.10
*2013-08-31* *2013-08-31*

View File

@ -1,22 +1,14 @@
module.exports = (grunt) -> module.exports = (grunt) ->
concatOptions =
process: Object.create(null, data:
get: -> grunt.config 'pkg'
enumerable: true
)
shellOptions =
stdout: true
stderr: true
failOnError: true
# Project configuration. # Project configuration.
grunt.initConfig grunt.initConfig
pkg: grunt.file.readJSON 'package.json' pkg: grunt.file.readJSON 'package.json'
concat: concat:
options: process: Object.create(null, data:
get: -> grunt.config 'pkg'
enumerable: true
)
coffee: coffee:
options: concatOptions
src: [ src: [
'src/General/Config.coffee' 'src/General/Config.coffee'
'src/General/Globals.coffee' 'src/General/Globals.coffee'
@ -41,13 +33,11 @@ module.exports = (grunt) ->
dest: 'tmp-<%= pkg.type %>/script.coffee' dest: 'tmp-<%= pkg.type %>/script.coffee'
meta: meta:
options: concatOptions
files: files:
'LICENSE': 'src/General/meta/banner.js', 'LICENSE': 'src/General/meta/banner.js',
'latest.js': 'src/General/meta/latest.js' 'latest.js': 'src/General/meta/latest.js'
crx: crx:
options: concatOptions
files: files:
'builds/crx/manifest.json': 'src/General/meta/manifest.json' 'builds/crx/manifest.json': 'src/General/meta/manifest.json'
'builds/crx/script.js': [ 'builds/crx/script.js': [
@ -57,7 +47,6 @@ module.exports = (grunt) ->
'tmp-<%= pkg.type %>/script.js' 'tmp-<%= pkg.type %>/script.js'
] ]
userscript: userscript:
options: concatOptions
files: files:
'builds/<%= pkg.name %>.meta.js': 'src/General/meta/metadata.js' 'builds/<%= pkg.name %>.meta.js': 'src/General/meta/metadata.js'
'builds/<%= pkg.name %>.user.js': [ 'builds/<%= pkg.name %>.user.js': [
@ -96,22 +85,23 @@ module.exports = (grunt) ->
push: false push: false
shell: shell:
options:
stdout: true
stderr: true
failOnError: true
commit: commit:
options: shellOptions
command: [ command: [
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."' 'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."'
'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."' 'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."'
'git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.version %>."' 'git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.version %>."'
].join ' && ' ].join ' && '
push: push:
options: shellOptions
command: 'git push origin --tags -f && git push origin --all' command: 'git push origin --tags -f && git push origin --all'
watch: watch:
options:
interrupt: true
all: all:
options:
interrupt: true
files: [ files: [
'Gruntfile.coffee' 'Gruntfile.coffee'
'package.json' 'package.json'

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.3.10 - 2013-09-20 * appchan x - Version 2.3.10 - 2013-09-24
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE

View File

@ -20,7 +20,7 @@
// ==/UserScript== // ==/UserScript==
/* /*
* appchan x - Version 2.3.10 - 2013-09-20 * appchan x - Version 2.3.10 - 2013-09-24
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@ -335,7 +335,7 @@
uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/", uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/",
tripcode: "# Filter any tripfag\n#/^!/", tripcode: "# Filter any tripfag\n#/^!/",
capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for moot:\n#/Admin$/;highlight:moot;op:yes", capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for moot:\n#/Admin$/;highlight:moot;op:yes",
email: "# Filter any e-mails that are not `sage` on /a/ and /jp/:\n#/^(?!sage$)/;boards:a,jp", email: "",
subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only", subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only",
comment: "# Filter Stallman copypasta on /g/:\n#/what you\'re refer+ing to as linux/i;boards:g", comment: "# Filter Stallman copypasta on /g/:\n#/what you\'re refer+ing to as linux/i;boards:g",
flag: '', flag: '',
@ -3320,7 +3320,7 @@
this.nodes.comment.normalize(); this.nodes.comment.normalize();
bq = this.nodes.comment.cloneNode(true); bq = this.nodes.comment.cloneNode(true);
nodes = $$('.abbr, .capcodeReplies, .exif, b', bq); nodes = $$('.abbr, .exif, b', bq);
i = 0; i = 0;
while (node = nodes[i++]) { while (node = nodes[i++]) {
$.rm(node); $.rm(node);
@ -3352,7 +3352,7 @@
return; return;
} }
this.nodes.quotelinks.push(quotelink); this.nodes.quotelinks.push(quotelink);
if (this.isClone || !this.isReply && $.hasClass(quotelink.parentNode.parentNode, 'capcodeReplies')) { if (this.isClone) {
return; return;
} }
fullID = "" + match[1] + "." + match[2]; fullID = "" + match[1] + "." + match[2];
@ -4224,7 +4224,6 @@
date: data.now, date: data.now,
dateUTC: data.time, dateUTC: data.time,
comment: data.com, comment: data.com,
capcodeReplies: data.capcode_replies,
isSticky: !!data.sticky, isSticky: !!data.sticky,
isClosed: !!data.closed isClosed: !!data.closed
}; };
@ -4252,9 +4251,9 @@
@license: https://github.com/4chan/4chan-JS/blob/master/LICENSE @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
*/ */
var a, boardID, capcode, capcodeClass, capcodeReplies, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref; var a, boardID, capcode, capcodeClass, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref;
postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, capcodeReplies = o.capcodeReplies, file = o.file; postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, file = o.file;
isOP = postID === threadID; isOP = postID === threadID;
staticPath = '//static.4chan.org/image/'; staticPath = '//static.4chan.org/image/';
if (email) { if (email) {
@ -4331,7 +4330,7 @@
container = $.el('div', { container = $.el('div', {
id: "pc" + postID, id: "pc" + postID,
className: "postContainer " + (isOP ? 'op' : 'reply') + "Container", className: "postContainer " + (isOP ? 'op' : 'reply') + "Container",
innerHTML: "" + (isOP ? '' : "<div class=sideArrows id=sa" + postID + ">&gt;&gt;</div>") + "<div id=p" + postID + " class='post " + (isOP ? 'op' : 'reply') + (capcode === 'admin_highlight' ? ' highlightPost' : '') + "'><div class='postInfoM mobile' id=pim" + postID + "><span class='nameBlock" + capcodeClass + "'><span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + capcode + userID + flag + sticky + closed) + "<br>" + subject + "</span><span class='dateTime postNum' data-utc=" + dateUTC + ">" + date + "<a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + ">No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "'>" + postID + "</a></span></div>" + (isOP ? fileHTML : '') + "<div class='postInfo desktop' id=pi" + postID + "><input type=checkbox name=" + postID + " value=delete>" + subject + "&nbsp;<span class='nameBlock" + capcodeClass + "'>" + emailStart + "<span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed) + "</span>" + " " + "<span class=dateTime data-utc=" + dateUTC + ">" + date + "</span>" + " " + "<span class='postNum desktop'><a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + " title='Highlight this post'>No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "' title='Quote this post'>" + postID + "</a></span></div>" + (isOP ? '' : fileHTML) + "<blockquote class=postMessage id=m" + postID + ">" + (comment || '') + "</blockquote>" + " " + "</div>" innerHTML: "" + (isOP ? '' : "<div class=sideArrows id=sa" + postID + ">&gt;&gt;</div>") + "<div id=p" + postID + " class='post " + (isOP ? 'op' : 'reply') + (capcode === 'admin_highlight' ? ' highlightPost' : '') + "'><div class='postInfoM mobile' id=pim" + postID + "><span class='nameBlock" + capcodeClass + "'><span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + capcode + userID + flag + sticky + closed) + "<br>" + subject + "</span><span class='dateTime postNum' data-utc=" + dateUTC + ">" + date + "<a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + ">No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "'>" + postID + "</a></span></div>" + (isOP ? fileHTML : '') + "<div class='postInfo desktop' id=pi" + postID + "><input type=checkbox name=" + postID + " value=delete>&nbsp;" + subject + "&nbsp;<span class='nameBlock" + capcodeClass + "'>" + emailStart + "<span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed) + "</span>" + " " + "<span class=dateTime data-utc=" + dateUTC + ">" + date + "</span>" + " " + "<span class='postNum desktop'><a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + " title='Highlight this post'>No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "' title='Quote this post'>" + postID + "</a></span></div>" + (isOP ? '' : fileHTML) + "<blockquote class=postMessage id=m" + postID + ">" + (comment || '') + "</blockquote>" + " " + "</div>"
}); });
_ref = $$('.quotelink', container); _ref = $$('.quotelink', container);
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@ -4342,47 +4341,7 @@
} }
quote.href = "/" + boardID + "/res/" + href; quote.href = "/" + boardID + "/res/" + href;
} }
Build.capcodeReplies({
boardID: boardID,
threadID: threadID,
root: container,
capcodeReplies: capcodeReplies
});
return container; return container;
},
capcodeReplies: function(_arg) {
var array, boardID, bq, capcodeReplies, capcodeType, generateCapcodeReplies, html, root, threadID;
boardID = _arg.boardID, threadID = _arg.threadID, bq = _arg.bq, root = _arg.root, capcodeReplies = _arg.capcodeReplies;
if (!capcodeReplies) {
return;
}
generateCapcodeReplies = function(capcodeType, array) {
return "<span class=smaller><span class=bold>" + ((function() {
switch (capcodeType) {
case 'admin':
return 'Administrator';
case 'mod':
return 'Moderator';
case 'developer':
return 'Developer';
}
})()) + " Repl" + (array.length > 1 ? 'ies' : 'y') + ":</span> " + (array.map(function(ID) {
return "<a href='/" + boardID + "/res/" + threadID + "#p" + ID + "' class=quotelink>&gt;&gt;" + ID + "</a>";
}).join(' ')) + "</span><br>";
};
html = [];
for (capcodeType in capcodeReplies) {
array = capcodeReplies[capcodeType];
html.push(generateCapcodeReplies(capcodeType, array));
}
bq || (bq = $('blockquote', root));
return $.add(bq, [
$.el('br'), $.el('br'), $.el('span', {
className: 'capcodeReplies',
innerHTML: html.join('')
})
]);
} }
}; };
@ -5109,12 +5068,8 @@
delete this.nodes.tripcode; delete this.nodes.tripcode;
} }
if (this.info.email) { if (this.info.email) {
if (/sage/i.test(this.info.email)) { $.replace(email, name);
return email.href = 'mailto:sage'; return delete this.nodes.email;
} else {
$.replace(email, name);
return delete this.nodes.email;
}
} }
} }
}; };
@ -5678,7 +5633,7 @@
return; return;
} }
a = PostHiding.makeButton(post, 'show'); a = PostHiding.makeButton(post, 'show');
postInfo = Conf['Anonymize'] ? 'Anonymous' : post.info.name; postInfo = Conf['Anonymize'] ? 'Anonymous' : $('.nameBlock', post.nodes.info).textContent;
$.add(a, $.tn(" " + postInfo)); $.add(a, $.tn(" " + postInfo));
post.nodes.stub = $.el('div', { post.nodes.stub = $.el('div', {
className: 'stub' className: 'stub'
@ -6026,7 +5981,7 @@
} }
numReplies = ((span = $('.summary', threadRoot)) ? +span.textContent.match(/\d+/) : 0) + $$('.opContainer ~ .replyContainer', threadRoot).length; numReplies = ((span = $('.summary', threadRoot)) ? +span.textContent.match(/\d+/) : 0) + $$('.opContainer ~ .replyContainer', threadRoot).length;
numReplies = numReplies === 1 ? '1 reply' : "" + (numReplies || 'no') + " replies"; numReplies = numReplies === 1 ? '1 reply' : "" + (numReplies || 'no') + " replies";
opInfo = Conf['Anonymize'] ? 'Anonymous' : OP.info.name; opInfo = Conf['Anonymize'] ? 'Anonymous' : $('.nameBlock', OP.nodes.info).textContent;
a = ThreadHiding.makeButton(thread, 'show'); a = ThreadHiding.makeButton(thread, 'show');
$.add(a, $.tn(" " + opInfo + " (" + numReplies + ")")); $.add(a, $.tn(" " + opInfo + " (" + numReplies + ")"));
thread.stub = $.el('div', { thread.stub = $.el('div', {
@ -7581,7 +7536,8 @@
persona = { persona = {
name: post.name, name: post.name,
email: /^sage$/.test(post.email) ? persona.email : post.email, email: /^sage$/.test(post.email) ? persona.email : post.email,
sub: Conf['Remember Subject'] ? post.sub : void 0 sub: Conf['Remember Subject'] ? post.sub : void 0,
flag: post.flag
}; };
return $.set('QR.persona', persona); return $.set('QR.persona', persona);
}); });
@ -7589,36 +7545,30 @@
}, },
cooldown: { cooldown: {
init: function() { init: function() {
var board; var key, setTimers, type, _base,
_this = this;
if (!Conf['Cooldown']) { if (!Conf['Cooldown']) {
return; return;
} }
board = g.BOARD.ID; setTimers = function(e) {
QR.cooldown.types = { return QR.cooldown.types = e.detail;
thread: (function() {
switch (board) {
case 'q':
return 86400;
case 'b':
case 'soc':
case 'r9k':
return 600;
default:
return 300;
}
})(),
sage: board === 'q' ? 600 : 60,
file: board === 'q' ? 300 : 30,
post: board === 'q' ? 150 : 30
}; };
$.on(window, 'cooldown:timers', setTimers);
$.globalEval('window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))');
(_base = QR.cooldown).types || (_base.types = {});
$.off(window, 'cooldown:timers', setTimers);
for (type in QR.cooldown.types) {
QR.cooldown.types[type] = +QR.cooldown.types[type];
}
QR.cooldown.upSpd = 0; QR.cooldown.upSpd = 0;
QR.cooldown.upSpdAccuracy = .5; QR.cooldown.upSpdAccuracy = .5;
$.get("cooldown." + board, {}, function(item) { key = "cooldown." + g.BOARD;
QR.cooldown.cooldowns = item["cooldown." + board]; $.get(key, {}, function(item) {
QR.cooldown.cooldowns = item[key];
return QR.cooldown.start(); return QR.cooldown.start();
}); });
return $.sync("cooldown." + board, QR.cooldown.sync); return $.sync(key, QR.cooldown.sync);
}, },
start: function() { start: function() {
if (!Conf['Cooldown']) { if (!Conf['Cooldown']) {
@ -7639,31 +7589,27 @@
return QR.cooldown.start(); return QR.cooldown.start();
}, },
set: function(data) { set: function(data) {
var cooldown, delay, hasFile, isReply, isSage, post, req, start, type, upSpd; var cooldown, delay, hasFile, isReply, post, req, start, threadID, upSpd;
if (!Conf['Cooldown']) { if (!Conf['Cooldown']) {
return; return;
} }
req = data.req, post = data.post, isReply = data.isReply, delay = data.delay; req = data.req, post = data.post, isReply = data.isReply, threadID = data.threadID, delay = data.delay;
start = req ? req.uploadEndTime : Date.now(); start = req ? req.uploadEndTime : Date.now();
if (delay) { if (delay) {
cooldown = { cooldown = {
delay: delay delay: delay
}; };
} else { } else {
if (post.file) { if (hasFile = !!post.file) {
upSpd = post.file.size / ((req.uploadEndTime - req.uploadStartTime) / $.SECOND); upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND);
QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2; QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2;
QR.cooldown.upSpd = upSpd; QR.cooldown.upSpd = upSpd;
} }
isSage = /sage/i.test(post.email);
hasFile = !!post.file;
type = !isReply ? 'thread' : isSage ? 'sage' : hasFile ? 'file' : 'post';
cooldown = { cooldown = {
isReply: isReply, isReply: isReply,
isSage: isSage,
hasFile: hasFile, hasFile: hasFile,
timeout: start + QR.cooldown.types[type] * $.SECOND threadID: threadID
}; };
} }
QR.cooldown.cooldowns[start] = cooldown; QR.cooldown.cooldowns[start] = cooldown;
@ -7679,7 +7625,7 @@
} }
}, },
count: function() { count: function() {
var cooldown, cooldowns, elapsed, hasFile, isReply, isSage, now, post, seconds, start, type, types, upSpd, upSpdAccuracy, update, _ref; var cooldown, cooldowns, elapsed, hasFile, isReply, maxTimer, now, post, seconds, start, type, types, upSpd, upSpdAccuracy, update, _ref;
if (!Object.keys(QR.cooldown.cooldowns).length) { if (!Object.keys(QR.cooldown.cooldowns).length) {
$["delete"]("" + g.BOARD + ".cooldown"); $["delete"]("" + g.BOARD + ".cooldown");
@ -7688,11 +7634,11 @@
QR.status(); QR.status();
return; return;
} }
setTimeout(QR.cooldown.count, $.SECOND); clearTimeout(QR.cooldown.timeout);
QR.cooldown.timeout = setTimeout(QR.cooldown.count, $.SECOND);
now = Date.now(); now = Date.now();
post = QR.posts[0]; post = QR.posts[0];
isReply = post.thread !== 'new'; isReply = post.thread !== 'new';
isSage = /sage/i.test(post.email);
hasFile = !!post.file; hasFile = !!post.file;
seconds = null; seconds = null;
_ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns, upSpd = _ref.upSpd, upSpdAccuracy = _ref.upSpdAccuracy; _ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns, upSpd = _ref.upSpd, upSpdAccuracy = _ref.upSpdAccuracy;
@ -7707,20 +7653,39 @@
} }
continue; continue;
} }
if (isReply === cooldown.isReply) { if ('timeout' in cooldown) {
type = !isReply ? 'thread' : isSage && cooldown.isSage ? 'sage' : hasFile && cooldown.hasFile ? 'file' : 'post';
elapsed = Math.floor((now - start) / $.SECOND);
if (elapsed >= 0) {
seconds = Math.max(seconds, types[type] - elapsed);
if (Conf['Cooldown Prediction'] && hasFile && upSpd) {
seconds -= Math.floor(post.file.size / upSpd * upSpdAccuracy);
seconds = Math.max(seconds, 0);
}
}
}
if (!((start <= now && now <= cooldown.timeout))) {
QR.cooldown.unset(start); QR.cooldown.unset(start);
continue;
} }
if (isReply === cooldown.isReply) {
elapsed = Math.floor((now - start) / $.SECOND);
if (elapsed < 0) {
continue;
}
if (!isReply) {
type = 'thread';
} else if (hasFile) {
if (!cooldown.hasFile) {
seconds = Math.max(seconds, 0);
continue;
}
type = 'image';
} else {
type = 'reply';
}
maxTimer = Math.max(types[type] || 0, types[type + '_intra'] || 0);
if (!((start <= now && now <= start + maxTimer * $.SECOND))) {
QR.cooldown.unset(start);
}
if (isReply && +post.thread === cooldown.threadID) {
type += '_intra';
}
seconds = Math.max(seconds, types[type] - elapsed);
}
}
if (seconds && Conf['Cooldown Prediction'] && hasFile && upSpd) {
seconds -= Math.floor(post.file.size / upSpd * upSpdAccuracy);
seconds = seconds > 0 ? seconds : 0;
} }
update = seconds !== null || !!QR.cooldown.seconds; update = seconds !== null || !!QR.cooldown.seconds;
QR.cooldown.seconds = seconds; QR.cooldown.seconds = seconds;
@ -7939,6 +7904,9 @@
_this.name = 'name' in QR.persona.always ? QR.persona.always.name : prev ? prev.name : persona.name; _this.name = 'name' in QR.persona.always ? QR.persona.always.name : prev ? prev.name : persona.name;
_this.email = 'email' in QR.persona.always ? QR.persona.always.email : prev && !/^sage$/.test(prev.email) ? prev.email : persona.email; _this.email = 'email' in QR.persona.always ? QR.persona.always.email : prev && !/^sage$/.test(prev.email) ? prev.email : persona.email;
_this.sub = 'sub' in QR.persona.always ? QR.persona.always.sub : Conf['Remember Subject'] ? prev ? prev.sub : persona.sub : ''; _this.sub = 'sub' in QR.persona.always ? QR.persona.always.sub : Conf['Remember Subject'] ? prev ? prev.sub : persona.sub : '';
if (QR.nodes.flag) {
_this.flag = prev ? prev.flag : persona.flag;
}
if (QR.selected === _this) { if (QR.selected === _this) {
return _this.load(); return _this.load();
} }
@ -7970,7 +7938,7 @@
}; };
_Class.prototype.lock = function(lock) { _Class.prototype.lock = function(lock) {
var name, _i, _len, _ref; var name, node, _i, _len, _ref;
if (lock == null) { if (lock == null) {
lock = true; lock = true;
@ -7979,10 +7947,12 @@
if (this !== QR.selected) { if (this !== QR.selected) {
return; return;
} }
_ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler']; _ref = ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i]; name = _ref[_i];
QR.nodes[name].disabled = lock; if (node = QR.nodes[name]) {
node.disabled = lock;
}
} }
this.nodes.rm.style.visibility = lock ? 'hidden' : ''; this.nodes.rm.style.visibility = lock ? 'hidden' : '';
(lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput); (lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput);
@ -8012,12 +7982,15 @@
}; };
_Class.prototype.load = function() { _Class.prototype.load = function() {
var name, _i, _len, _ref; var name, node, _i, _len, _ref;
_ref = ['thread', 'name', 'email', 'sub', 'com', 'filename']; _ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i]; name = _ref[_i];
QR.nodes[name].value = this[name] || null; if (!(node = QR.nodes[name])) {
continue;
}
node.value = this[name] || node.dataset["default"] || null;
} }
QR.tripcodeHider.call(QR.nodes['name']); QR.tripcodeHider.call(QR.nodes['name']);
this.showFileData(); this.showFileData();
@ -8032,7 +8005,7 @@
return; return;
} }
name = input.dataset.name; name = input.dataset.name;
this[name] = input.value; this[name] = input.value || input.dataset["default"] || null;
switch (name) { switch (name) {
case 'thread': case 'thread':
return QR.status(); return QR.status();
@ -8056,15 +8029,18 @@
}; };
_Class.prototype.forceSave = function() { _Class.prototype.forceSave = function() {
var name, _i, _len, _ref; var name, node, _i, _len, _ref;
if (this !== QR.selected) { if (this !== QR.selected) {
return; return;
} }
_ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler']; _ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler', 'flag'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i]; name = _ref[_i];
this.save(QR.nodes[name]); if (!(node = QR.nodes[name])) {
continue;
}
this.save(node);
} }
}; };
@ -8402,7 +8378,7 @@
} }
}, },
dialog: function() { dialog: function() {
var check, dialog, elm, i, items, key, mimeTypes, name, nodes, thread, value, _ref; var check, dialog, elm, event, flagSelector, i, items, key, mimeTypes, name, node, nodes, save, thread, value, _ref;
QR.nodes = nodes = { QR.nodes = nodes = {
el: dialog = UI.dialog('qr', 'top:0;right:0;', "<div id=qrtab class=move><input type=checkbox id=autohide title=Auto-hide><div id=qr-thread-select><select data-name=thread title='Create a new thread / Reply'><option value=new>New thread</option></select></div><a href=javascript:; class=close title=Close>✖</a></div><form><div class=persona><input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1 tabindex=10><input name=email data-name=email list=\"list-email\" placeholder=E-mail class=field size=1 tabindex=20><input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1 tabindex=30></div><div class=textarea><textarea data-name=com placeholder=Comment class=field tabindex=40></textarea><span id=char-count></span></div><div id=dump-list-container><div id=dump-list></div><a id=add-post href=javascript:; title=\"Add a post\" tabindex=50>+</a></div><div id=file-n-submit><span id=qr-filename-container class=field tabindex=60><span id=qr-no-file>No selected file</span><input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\"><span id=qr-extras-container><label id=qr-spoiler-label><input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=70></label><span class=description>Spoiler</span><a id=dump-button title='Dump list'>+</a><span class=description>Dump</span><a id=qr-filerm href=javascript:; title='Remove file'>✖</a><span class=description>Remove File</span></span></span><input type=submit tabindex=80></div><input type=file multiple></form><datalist id=\"list-name\"></datalist><datalist id=\"list-email\"></datalist><datalist id=\"list-sub\"></datalist>") el: dialog = UI.dialog('qr', 'top:0;right:0;', "<div id=qrtab class=move><input type=checkbox id=autohide title=Auto-hide><div id=qr-thread-select><select data-name=thread title='Create a new thread / Reply'><option value=new>New thread</option></select></div><a href=javascript:; class=close title=Close>✖</a></div><form><div class=persona><input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1 tabindex=10><input name=email data-name=email list=\"list-email\" placeholder=E-mail class=field size=1 tabindex=20><input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1 tabindex=30></div><div class=textarea><textarea data-name=com placeholder=Comment class=field tabindex=40></textarea><span id=char-count></span></div><div id=dump-list-container><div id=dump-list></div><a id=add-post href=javascript:; title=\"Add a post\" tabindex=50>+</a></div><div id=file-n-submit><span id=qr-filename-container class=field tabindex=60><span id=qr-no-file>No selected file</span><input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\"><span id=qr-extras-container><label id=qr-spoiler-label><input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=70></label><span class=description>Spoiler</span><a id=dump-button title='Dump list'>+</a><span class=description>Dump</span><a id=qr-filerm href=javascript:; title='Remove file'>✖</a><span class=description>Remove File</span></span></span><input type=submit tabindex=80></div><input type=file multiple></form><datalist id=\"list-name\"></datalist><datalist id=\"list-email\"></datalist><datalist id=\"list-sub\"></datalist>")
@ -8462,8 +8438,15 @@
name: 'filetag', 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>" 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>"
}); });
nodes.flashTag.dataset["default"] = '4';
$.add(nodes.form, nodes.flashTag); $.add(nodes.form, nodes.flashTag);
} }
if (flagSelector = $('.flagSelector')) {
nodes.flag = flagSelector.cloneNode(true);
nodes.flag.dataset.name = 'flag';
nodes.flag.dataset["default"] = '0';
$.add(nodes.form, nodes.flag);
}
for (thread in g.BOARD.threads) { for (thread in g.BOARD.threads) {
$.add(nodes.thread, $.el('option', { $.add(nodes.thread, $.el('option', {
value: thread, value: thread,
@ -8506,12 +8489,17 @@
while (name = items[i++]) { while (name = items[i++]) {
$.on(nodes[name], 'mouseover', QR.mouseover); $.on(nodes[name], 'mouseover', QR.mouseover);
} }
items = ['name', 'email', 'sub', 'com', 'filename']; items = ['name', 'email', 'sub', 'com', 'filename', 'flag'];
i = 0; i = 0;
save = function() {
return QR.selected.save(this);
};
while (name = items[i++]) { while (name = items[i++]) {
$.on(nodes[name], 'input', function() { if (!(node = nodes[name])) {
return QR.selected.save(this); continue;
}); }
event = node.nodeName === 'SELECT' ? 'change' : 'input';
$.on(nodes[name], event, save);
} }
$.on(nodes['name'], 'blur', QR.tripcodeHider); $.on(nodes['name'], 'blur', QR.tripcodeHider);
$.on(nodes.thread, 'change', function() { $.on(nodes.thread, 'change', function() {
@ -8552,7 +8540,7 @@
}, },
preSubmitHooks: [], preSubmitHooks: [],
submit: function(e) { submit: function(e) {
var challenge, err, extra, filetag, hook, options, post, postData, response, textOnly, thread, threadID, _i, _len, _ref, _ref1; var challenge, err, extra, filetag, formData, hook, options, post, response, textOnly, thread, threadID, _i, _len, _ref, _ref1;
if (e != null) { if (e != null) {
e.preventDefault(); e.preventDefault();
@ -8575,7 +8563,7 @@
thread = g.BOARD.threads[threadID]; thread = g.BOARD.threads[threadID];
if (threadID === 'new') { if (threadID === 'new') {
threadID = null; threadID = null;
if (['vg', 'q'].contains(g.BOARD.ID) && !post.sub) { if (g.BOARD.ID === 'vg' && !post.sub) {
err = 'New threads require a subject.'; err = 'New threads require a subject.';
} else if (!(post.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) { } else if (!(post.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) {
err = 'No file selected.'; err = 'No file selected.';
@ -8616,7 +8604,7 @@
d.activeElement.blur(); d.activeElement.blur();
} }
post.lock(); post.lock();
postData = { formData = {
resto: threadID, resto: threadID,
name: post.name, name: post.name,
email: post.email, email: post.email,
@ -8625,6 +8613,7 @@
upfile: post.file, upfile: post.file,
filetag: filetag, filetag: filetag,
spoiler: post.spoiler, spoiler: post.spoiler,
flag: post.flag,
textonly: textOnly, textonly: textOnly,
mode: 'regist', mode: 'regist',
pwd: QR.persona.pwd, pwd: QR.persona.pwd,
@ -8646,7 +8635,7 @@
} }
}; };
extra = { extra = {
form: $.formData(postData), form: $.formData(formData),
upCallbacks: { upCallbacks: {
onload: function() { onload: function() {
QR.req.isUploadFinished = true; QR.req.isUploadFinished = true;
@ -8730,8 +8719,8 @@
threadID: threadID, threadID: threadID,
postID: postID postID: postID
}); });
postsCount = QR.posts.length; postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount > 1 && isReply; QR.cooldown.auto = postsCount && isReply;
if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) { if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
notif = new Notification('Quick reply warning', { notif = new Notification('Quick reply warning', {
body: "You are running low on cached captchas. Cache count: " + captchasCount + ".", body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
@ -8756,7 +8745,8 @@
QR.cooldown.set({ QR.cooldown.set({
req: req, req: req,
post: post, post: post,
isReply: isReply isReply: isReply,
threadID: threadID
}); });
URL = !isReply ? "/" + g.BOARD + "/res/" + threadID : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? "/" + g.BOARD + "/res/" + threadID + "#p" + postID : void 0; URL = !isReply ? "/" + g.BOARD + "/res/" + threadID : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? "/" + g.BOARD + "/res/" + threadID + "#p" + postID : void 0;
if (URL) { if (URL) {
@ -9787,7 +9777,7 @@
open: function(post) { open: function(post) {
var node; var node;
if (post.isDead || post.board.ID === 'q') { if (post.isDead) {
return false; return false;
} }
DeleteLink.post = post; DeleteLink.post = post;
@ -9862,7 +9852,7 @@
return; return;
} }
DeleteLink.cooldown.counting = post; DeleteLink.cooldown.counting = post;
length = 30; length = 60;
seconds = Math.ceil((length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND); seconds = Math.ceil((length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND);
return DeleteLink.cooldown.count(post, seconds, length, node); return DeleteLink.cooldown.count(post, seconds, length, node);
}, },
@ -10158,7 +10148,6 @@
if (g.VIEW !== 'thread' || !Conf['Thread Updater']) { if (g.VIEW !== 'thread' || !Conf['Thread Updater']) {
return; return;
} }
checked = Conf['Auto Update'] ? 'checked' : '';
if (Conf['Updater and Stats in Header']) { if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', { this.dialog = sc = $.el('span', {
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0", innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0",
@ -10178,6 +10167,7 @@
this.checkPostCount = 0; this.checkPostCount = 0;
this.timer = $('#update-timer', sc); this.timer = $('#update-timer', sc);
this.status = $('#update-status', sc); this.status = $('#update-status', sc);
this.isUpdating = Conf['Auto Update'];
$.on(this.timer, 'click', ThreadUpdater.update); $.on(this.timer, 'click', ThreadUpdater.update);
$.on(this.status, 'click', ThreadUpdater.update); $.on(this.status, 'click', ThreadUpdater.update);
subEntries = []; subEntries = [];
@ -10195,7 +10185,7 @@
$.on(input, 'change', ThreadUpdater.cb.scrollBG); $.on(input, 'change', ThreadUpdater.cb.scrollBG);
ThreadUpdater.cb.scrollBG(); ThreadUpdater.cb.scrollBG();
} else if (input.name === 'Auto Update') { } else if (input.name === 'Auto Update') {
$.on(input, 'change', ThreadUpdater.update); $.on(input, 'change', ThreadUpdater.cb.update);
} }
subEntries.push({ subEntries.push({
el: el el: el
@ -10225,7 +10215,6 @@
ThreadUpdater.thread = this; ThreadUpdater.thread = this;
ThreadUpdater.root = this.OP.nodes.root.parentNode; ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]; ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0];
ThreadUpdater.outdateCount = 0;
ThreadUpdater.cb.interval.call($.el('input', { ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval'] value: Conf['Interval']
})); }));
@ -10243,19 +10232,18 @@
beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA', beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA',
cb: { cb: {
online: function() { online: function() {
if (ThreadUpdater.online = navigator.onLine) { if (navigator.onLine) {
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
ThreadUpdater.setInterval(); ThreadUpdater.setInterval();
ThreadUpdater.update();
ThreadUpdater.set('status', null, null); ThreadUpdater.set('status', null, null);
} else { } else {
ThreadUpdater.set('timer', null); ThreadUpdater.set('timer', null);
ThreadUpdater.set('status', 'Offline', 'warning'); ThreadUpdater.set('status', 'Offline', 'warning');
} }
return ThreadUpdater.cb.autoUpdate(); return ThreadUpdater.count(true);
}, },
post: function(e) { post: function(e) {
if (e.detail.threadID !== ThreadUpdater.thread.ID) { if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) {
return; return;
} }
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
@ -10285,9 +10273,7 @@
return; return;
} }
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
if (ThreadUpdater.seconds > ThreadUpdater.interval) { return ThreadUpdater.seconds = Math.min(ThreadUpdater.seconds, ThreadUpdater.interval);
return ThreadUpdater.setInterval();
}
}, },
scrollBG: function() { scrollBG: function() {
return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? function() { return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? function() {
@ -10296,17 +10282,10 @@
return !d.hidden; return !d.hidden;
}; };
}, },
autoUpdate: function() {
if (ThreadUpdater.online) {
return ThreadUpdater.timeout();
} else {
return clearTimeout(ThreadUpdater.timeoutID);
}
},
interval: function() { interval: function() {
var val; var val;
val = +this.value; val = parseInt(this.value, 10);
if (val < 1) { if (val < 1) {
val = 1; val = 1;
} }
@ -10317,9 +10296,9 @@
var klass, req, text, _ref; var klass, req, text, _ref;
req = ThreadUpdater.req; req = ThreadUpdater.req;
delete ThreadUpdater.req;
if (e.type !== 'loadend') { if (e.type !== 'loadend') {
req.onloadend = null; req.onloadend = null;
delete ThreadUpdater.req;
if (e.type === 'timeout') { if (e.type === 'timeout') {
ThreadUpdater.set('status', 'Retrying', null); ThreadUpdater.set('status', 'Retrying', null);
ThreadUpdater.update(); ThreadUpdater.update();
@ -10349,9 +10328,8 @@
ThreadUpdater.set('status', text, klass); ThreadUpdater.set('status', text, klass);
} }
if (ThreadUpdater.postID) { if (ThreadUpdater.postID) {
ThreadUpdater.cb.checkpost(); return ThreadUpdater.cb.checkpost();
} }
return delete ThreadUpdater.req;
} }
}, },
setInterval: function() { setInterval: function() {
@ -10364,8 +10342,7 @@
} }
ThreadUpdater.seconds = Conf['Optional Increase'] ? (cur = [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] > i) ? cur : i : i; ThreadUpdater.seconds = Conf['Optional Increase'] ? (cur = [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] > i) ? cur : i : i;
ThreadUpdater.set('timer', ThreadUpdater.seconds++); ThreadUpdater.set('timer', ThreadUpdater.seconds++);
clearTimeout(ThreadUpdater.timeoutID); return ThreadUpdater.count(true);
return ThreadUpdater.timeout();
}, },
intervalShortcut: function() { intervalShortcut: function() {
var settings; var settings;
@ -10387,20 +10364,29 @@
return el.className = klass; return el.className = klass;
} }
}, },
count: function(start) {
clearTimeout(ThreadUpdater.timeoutID);
if (start && ThreadUpdater.isUpdating && navigator.onLine) {
return ThreadUpdater.timeout();
}
},
timeout: function() { timeout: function() {
var sec;
ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000);
ThreadUpdater.set('timer', --ThreadUpdater.seconds); sec = ThreadUpdater.seconds--;
if (ThreadUpdater.seconds <= 0) { ThreadUpdater.set('timer', sec);
if (sec <= 0) {
return ThreadUpdater.update(); return ThreadUpdater.update();
} }
}, },
update: function() { update: function() {
var url; var url;
if (!ThreadUpdater.online) { if (!navigator.onLine) {
return; return;
} }
clearTimeout(ThreadUpdater.timeoutID); ThreadUpdater.count();
if (Conf['Auto Update']) { if (Conf['Auto Update']) {
ThreadUpdater.set('timer', '...'); ThreadUpdater.set('timer', '...');
} else { } else {
@ -10574,27 +10560,6 @@
ThreadWatcher.fetchAllStatus(); ThreadWatcher.fetchAllStatus();
this.db.save(); this.db.save();
} }
$.get('WatchedThreads', null, function(_arg) {
var WatchedThreads, boardID, data, threadID, threads, _ref;
WatchedThreads = _arg.WatchedThreads;
if (!WatchedThreads) {
return;
}
_ref = ThreadWatcher.convert(WatchedThreads);
for (boardID in _ref) {
threads = _ref[boardID];
for (threadID in threads) {
data = threads[threadID];
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
}
}
return $["delete"]('WatchedThreads');
});
return Thread.prototype.callbacks.push({ return Thread.prototype.callbacks.push({
name: 'Thread Watcher', name: 'Thread Watcher',
cb: this.node cb: this.node
@ -11375,6 +11340,7 @@
'4plebs': { '4plebs': {
domain: 'archive.4plebs.org', domain: 'archive.4plebs.org',
http: true, http: true,
https: true,
software: 'foolfuuka', software: 'foolfuuka',
boards: ['hr', 'tg', 'tv', 'x'], boards: ['hr', 'tg', 'tv', 'x'],
files: ['hr', 'tg', 'tv', 'x'] files: ['hr', 'tg', 'tv', 'x']
@ -11382,10 +11348,10 @@
'fap archive': { 'fap archive': {
domain: 'fuuka.worldathleticproject.org', domain: 'fuuka.worldathleticproject.org',
http: true, http: true,
https: false, https: true,
software: 'foolfuuka', software: 'foolfuuka',
boards: ['b', 'e', 'h', 'hc', 'p', 's', 'u'], boards: ['b', 'e', 'h', 'hc', 'p', 's', 'u'],
files: ['b', 'e', 'h', 'hc', 'p', 's', 'u'] files: ['b', 'e', 'h', 'hc', 'p', 's', 'soc', 'sp', 'u']
}, },
'Foolz': { 'Foolz': {
domain: 'archive.foolz.us', domain: 'archive.foolz.us',
@ -11409,8 +11375,8 @@
https: true, https: true,
withCredentials: true, withCredentials: true,
software: 'foolfuuka', software: 'foolfuuka',
boards: ['a', 'co', 'gd', 'h', 'jp', 'm', 'mlp', 'q', 'sp', 'tg', 'tv', 'u', 'v', 'vg', 'vp', 'vr', 'wsg'], boards: ['a', 'co', 'd', 'gd', 'h', 'jp', 'm', 'mlp', 'q', 'sp', 'tg', 'tv', 'u', 'v', 'vg', 'vp', 'vr', 'wsg'],
files: ['a', 'gd', 'h', 'jp', 'm', 'q', 'tg', 'u', 'vg', 'vp', 'vr', 'wsg'] files: ['a', 'd', 'gd', 'h', 'jp', 'm', 'q', 'tg', 'u', 'vg', 'vp', 'vr', 'wsg']
}, },
'Heinessen': { 'Heinessen': {
domain: 'archive.heinessen.com', domain: 'archive.heinessen.com',
@ -11500,14 +11466,10 @@
return "" + (Redirect.protocol(archive)) + archive.domain + "/" + path; return "" + (Redirect.protocol(archive)) + archive.domain + "/" + path;
}, },
post: function(archive, _arg) { post: function(archive, _arg) {
var URL, boardID, postID, protocol; var URL, boardID, postID;
boardID = _arg.boardID, postID = _arg.postID; boardID = _arg.boardID, postID = _arg.postID;
protocol = Redirect.protocol(archive); URL = new String("" + (Redirect.protocol(archive)) + archive.domain + "/_/api/chan/post/?board=" + boardID + "&num=" + postID);
if (['Foolz', 'NSFW Foolz'].contains(archive.name)) {
protocol = 'https://';
}
URL = new String("" + protocol + archive.domain + "/_/api/chan/post/?board=" + boardID + "&num=" + postID);
URL.archive = archive; URL.archive = archive;
return URL; return URL;
}, },
@ -13418,12 +13380,6 @@
} }
quote.href = "/" + post.board + "/res/" + href; quote.href = "/" + post.board + "/res/" + href;
} }
Build.capcodeReplies({
boardID: post.board.ID,
threadID: post.thread.ID,
bq: clone,
capcodeReplies: postObj.capcode_replies
});
post.nodes.shortComment = comment; post.nodes.shortComment = comment;
$.replace(comment, clone); $.replace(comment, clone);
post.nodes.comment = post.nodes.longComment = clone; post.nodes.comment = post.nodes.longComment = clone;
@ -13509,7 +13465,6 @@
switch (g.BOARD.ID) { switch (g.BOARD.ID) {
case 'b': case 'b':
case 'vg': case 'vg':
case 'q':
return 3; return 3;
case 't': case 't':
return 1; return 1;

View File

@ -1,6 +1,6 @@
// Generated by CoffeeScript // Generated by CoffeeScript
/* /*
* appchan x - Version 2.3.10 - 2013-09-20 * appchan x - Version 2.3.10 - 2013-09-24
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@ -317,7 +317,7 @@
uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/", uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/",
tripcode: "# Filter any tripfag\n#/^!/", tripcode: "# Filter any tripfag\n#/^!/",
capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for moot:\n#/Admin$/;highlight:moot;op:yes", capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for moot:\n#/Admin$/;highlight:moot;op:yes",
email: "# Filter any e-mails that are not `sage` on /a/ and /jp/:\n#/^(?!sage$)/;boards:a,jp", email: "",
subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only", subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only",
comment: "# Filter Stallman copypasta on /g/:\n#/what you\'re refer+ing to as linux/i;boards:g", comment: "# Filter Stallman copypasta on /g/:\n#/what you\'re refer+ing to as linux/i;boards:g",
flag: '', flag: '',
@ -3332,7 +3332,7 @@
this.nodes.comment.normalize(); this.nodes.comment.normalize();
bq = this.nodes.comment.cloneNode(true); bq = this.nodes.comment.cloneNode(true);
nodes = $$('.abbr, .capcodeReplies, .exif, b', bq); nodes = $$('.abbr, .exif, b', bq);
i = 0; i = 0;
while (node = nodes[i++]) { while (node = nodes[i++]) {
$.rm(node); $.rm(node);
@ -3364,7 +3364,7 @@
return; return;
} }
this.nodes.quotelinks.push(quotelink); this.nodes.quotelinks.push(quotelink);
if (this.isClone || !this.isReply && $.hasClass(quotelink.parentNode.parentNode, 'capcodeReplies')) { if (this.isClone) {
return; return;
} }
fullID = "" + match[1] + "." + match[2]; fullID = "" + match[1] + "." + match[2];
@ -4241,7 +4241,6 @@
date: data.now, date: data.now,
dateUTC: data.time, dateUTC: data.time,
comment: data.com, comment: data.com,
capcodeReplies: data.capcode_replies,
isSticky: !!data.sticky, isSticky: !!data.sticky,
isClosed: !!data.closed isClosed: !!data.closed
}; };
@ -4269,9 +4268,9 @@
@license: https://github.com/4chan/4chan-JS/blob/master/LICENSE @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
*/ */
var a, boardID, capcode, capcodeClass, capcodeReplies, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref; var a, boardID, capcode, capcodeClass, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref;
postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, capcodeReplies = o.capcodeReplies, file = o.file; postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, file = o.file;
isOP = postID === threadID; isOP = postID === threadID;
staticPath = '//static.4chan.org/image/'; staticPath = '//static.4chan.org/image/';
if (email) { if (email) {
@ -4348,7 +4347,7 @@
container = $.el('div', { container = $.el('div', {
id: "pc" + postID, id: "pc" + postID,
className: "postContainer " + (isOP ? 'op' : 'reply') + "Container", className: "postContainer " + (isOP ? 'op' : 'reply') + "Container",
innerHTML: "" + (isOP ? '' : "<div class=sideArrows id=sa" + postID + ">&gt;&gt;</div>") + "<div id=p" + postID + " class='post " + (isOP ? 'op' : 'reply') + (capcode === 'admin_highlight' ? ' highlightPost' : '') + "'><div class='postInfoM mobile' id=pim" + postID + "><span class='nameBlock" + capcodeClass + "'><span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + capcode + userID + flag + sticky + closed) + "<br>" + subject + "</span><span class='dateTime postNum' data-utc=" + dateUTC + ">" + date + "<a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + ">No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "'>" + postID + "</a></span></div>" + (isOP ? fileHTML : '') + "<div class='postInfo desktop' id=pi" + postID + "><input type=checkbox name=" + postID + " value=delete>" + subject + "&nbsp;<span class='nameBlock" + capcodeClass + "'>" + emailStart + "<span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed) + "</span>" + " " + "<span class=dateTime data-utc=" + dateUTC + ">" + date + "</span>" + " " + "<span class='postNum desktop'><a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + " title='Highlight this post'>No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "' title='Quote this post'>" + postID + "</a></span></div>" + (isOP ? '' : fileHTML) + "<blockquote class=postMessage id=m" + postID + ">" + (comment || '') + "</blockquote>" + " " + "</div>" innerHTML: "" + (isOP ? '' : "<div class=sideArrows id=sa" + postID + ">&gt;&gt;</div>") + "<div id=p" + postID + " class='post " + (isOP ? 'op' : 'reply') + (capcode === 'admin_highlight' ? ' highlightPost' : '') + "'><div class='postInfoM mobile' id=pim" + postID + "><span class='nameBlock" + capcodeClass + "'><span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + capcode + userID + flag + sticky + closed) + "<br>" + subject + "</span><span class='dateTime postNum' data-utc=" + dateUTC + ">" + date + "<a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + ">No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "'>" + postID + "</a></span></div>" + (isOP ? fileHTML : '') + "<div class='postInfo desktop' id=pi" + postID + "><input type=checkbox name=" + postID + " value=delete>&nbsp;" + subject + "&nbsp;<span class='nameBlock" + capcodeClass + "'>" + emailStart + "<span class=name>" + (name || '') + "</span>" + (tripcode + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed) + "</span>" + " " + "<span class=dateTime data-utc=" + dateUTC + ">" + date + "</span>" + " " + "<span class='postNum desktop'><a href=" + ("/" + boardID + "/res/" + threadID + "#p" + postID) + " title='Highlight this post'>No.</a><a href='" + (g.VIEW === 'thread' && g.THREADID === +threadID ? "javascript:quote(" + postID + ")" : "/" + boardID + "/res/" + threadID + "#q" + postID) + "' title='Quote this post'>" + postID + "</a></span></div>" + (isOP ? '' : fileHTML) + "<blockquote class=postMessage id=m" + postID + ">" + (comment || '') + "</blockquote>" + " " + "</div>"
}); });
_ref = $$('.quotelink', container); _ref = $$('.quotelink', container);
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@ -4359,47 +4358,7 @@
} }
quote.href = "/" + boardID + "/res/" + href; quote.href = "/" + boardID + "/res/" + href;
} }
Build.capcodeReplies({
boardID: boardID,
threadID: threadID,
root: container,
capcodeReplies: capcodeReplies
});
return container; return container;
},
capcodeReplies: function(_arg) {
var array, boardID, bq, capcodeReplies, capcodeType, generateCapcodeReplies, html, root, threadID;
boardID = _arg.boardID, threadID = _arg.threadID, bq = _arg.bq, root = _arg.root, capcodeReplies = _arg.capcodeReplies;
if (!capcodeReplies) {
return;
}
generateCapcodeReplies = function(capcodeType, array) {
return "<span class=smaller><span class=bold>" + ((function() {
switch (capcodeType) {
case 'admin':
return 'Administrator';
case 'mod':
return 'Moderator';
case 'developer':
return 'Developer';
}
})()) + " Repl" + (array.length > 1 ? 'ies' : 'y') + ":</span> " + (array.map(function(ID) {
return "<a href='/" + boardID + "/res/" + threadID + "#p" + ID + "' class=quotelink>&gt;&gt;" + ID + "</a>";
}).join(' ')) + "</span><br>";
};
html = [];
for (capcodeType in capcodeReplies) {
array = capcodeReplies[capcodeType];
html.push(generateCapcodeReplies(capcodeType, array));
}
bq || (bq = $('blockquote', root));
return $.add(bq, [
$.el('br'), $.el('br'), $.el('span', {
className: 'capcodeReplies',
innerHTML: html.join('')
})
]);
} }
}; };
@ -5119,12 +5078,8 @@
delete this.nodes.tripcode; delete this.nodes.tripcode;
} }
if (this.info.email) { if (this.info.email) {
if (/sage/i.test(this.info.email)) { $.replace(email, name);
return email.href = 'mailto:sage'; return delete this.nodes.email;
} else {
$.replace(email, name);
return delete this.nodes.email;
}
} }
} }
}; };
@ -5688,7 +5643,7 @@
return; return;
} }
a = PostHiding.makeButton(post, 'show'); a = PostHiding.makeButton(post, 'show');
postInfo = Conf['Anonymize'] ? 'Anonymous' : post.info.name; postInfo = Conf['Anonymize'] ? 'Anonymous' : $('.nameBlock', post.nodes.info).textContent;
$.add(a, $.tn(" " + postInfo)); $.add(a, $.tn(" " + postInfo));
post.nodes.stub = $.el('div', { post.nodes.stub = $.el('div', {
className: 'stub' className: 'stub'
@ -6036,7 +5991,7 @@
} }
numReplies = ((span = $('.summary', threadRoot)) ? +span.textContent.match(/\d+/) : 0) + $$('.opContainer ~ .replyContainer', threadRoot).length; numReplies = ((span = $('.summary', threadRoot)) ? +span.textContent.match(/\d+/) : 0) + $$('.opContainer ~ .replyContainer', threadRoot).length;
numReplies = numReplies === 1 ? '1 reply' : "" + (numReplies || 'no') + " replies"; numReplies = numReplies === 1 ? '1 reply' : "" + (numReplies || 'no') + " replies";
opInfo = Conf['Anonymize'] ? 'Anonymous' : OP.info.name; opInfo = Conf['Anonymize'] ? 'Anonymous' : $('.nameBlock', OP.nodes.info).textContent;
a = ThreadHiding.makeButton(thread, 'show'); a = ThreadHiding.makeButton(thread, 'show');
$.add(a, $.tn(" " + opInfo + " (" + numReplies + ")")); $.add(a, $.tn(" " + opInfo + " (" + numReplies + ")"));
thread.stub = $.el('div', { thread.stub = $.el('div', {
@ -7596,7 +7551,8 @@
persona = { persona = {
name: post.name, name: post.name,
email: /^sage$/.test(post.email) ? persona.email : post.email, email: /^sage$/.test(post.email) ? persona.email : post.email,
sub: Conf['Remember Subject'] ? post.sub : void 0 sub: Conf['Remember Subject'] ? post.sub : void 0,
flag: post.flag
}; };
return $.set('QR.persona', persona); return $.set('QR.persona', persona);
}); });
@ -7604,36 +7560,30 @@
}, },
cooldown: { cooldown: {
init: function() { init: function() {
var board; var key, setTimers, type, _base,
_this = this;
if (!Conf['Cooldown']) { if (!Conf['Cooldown']) {
return; return;
} }
board = g.BOARD.ID; setTimers = function(e) {
QR.cooldown.types = { return QR.cooldown.types = e.detail;
thread: (function() {
switch (board) {
case 'q':
return 86400;
case 'b':
case 'soc':
case 'r9k':
return 600;
default:
return 300;
}
})(),
sage: board === 'q' ? 600 : 60,
file: board === 'q' ? 300 : 30,
post: board === 'q' ? 150 : 30
}; };
$.on(window, 'cooldown:timers', setTimers);
$.globalEval('window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))');
(_base = QR.cooldown).types || (_base.types = {});
$.off(window, 'cooldown:timers', setTimers);
for (type in QR.cooldown.types) {
QR.cooldown.types[type] = +QR.cooldown.types[type];
}
QR.cooldown.upSpd = 0; QR.cooldown.upSpd = 0;
QR.cooldown.upSpdAccuracy = .5; QR.cooldown.upSpdAccuracy = .5;
$.get("cooldown." + board, {}, function(item) { key = "cooldown." + g.BOARD;
QR.cooldown.cooldowns = item["cooldown." + board]; $.get(key, {}, function(item) {
QR.cooldown.cooldowns = item[key];
return QR.cooldown.start(); return QR.cooldown.start();
}); });
return $.sync("cooldown." + board, QR.cooldown.sync); return $.sync(key, QR.cooldown.sync);
}, },
start: function() { start: function() {
if (!Conf['Cooldown']) { if (!Conf['Cooldown']) {
@ -7654,31 +7604,27 @@
return QR.cooldown.start(); return QR.cooldown.start();
}, },
set: function(data) { set: function(data) {
var cooldown, delay, hasFile, isReply, isSage, post, req, start, type, upSpd; var cooldown, delay, hasFile, isReply, post, req, start, threadID, upSpd;
if (!Conf['Cooldown']) { if (!Conf['Cooldown']) {
return; return;
} }
req = data.req, post = data.post, isReply = data.isReply, delay = data.delay; req = data.req, post = data.post, isReply = data.isReply, threadID = data.threadID, delay = data.delay;
start = req ? req.uploadEndTime : Date.now(); start = req ? req.uploadEndTime : Date.now();
if (delay) { if (delay) {
cooldown = { cooldown = {
delay: delay delay: delay
}; };
} else { } else {
if (post.file) { if (hasFile = !!post.file) {
upSpd = post.file.size / ((req.uploadEndTime - req.uploadStartTime) / $.SECOND); upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND);
QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2; QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2;
QR.cooldown.upSpd = upSpd; QR.cooldown.upSpd = upSpd;
} }
isSage = /sage/i.test(post.email);
hasFile = !!post.file;
type = !isReply ? 'thread' : isSage ? 'sage' : hasFile ? 'file' : 'post';
cooldown = { cooldown = {
isReply: isReply, isReply: isReply,
isSage: isSage,
hasFile: hasFile, hasFile: hasFile,
timeout: start + QR.cooldown.types[type] * $.SECOND threadID: threadID
}; };
} }
QR.cooldown.cooldowns[start] = cooldown; QR.cooldown.cooldowns[start] = cooldown;
@ -7694,7 +7640,7 @@
} }
}, },
count: function() { count: function() {
var cooldown, cooldowns, elapsed, hasFile, isReply, isSage, now, post, seconds, start, type, types, upSpd, upSpdAccuracy, update, _ref; var cooldown, cooldowns, elapsed, hasFile, isReply, maxTimer, now, post, seconds, start, type, types, upSpd, upSpdAccuracy, update, _ref;
if (!Object.keys(QR.cooldown.cooldowns).length) { if (!Object.keys(QR.cooldown.cooldowns).length) {
$["delete"]("" + g.BOARD + ".cooldown"); $["delete"]("" + g.BOARD + ".cooldown");
@ -7703,11 +7649,11 @@
QR.status(); QR.status();
return; return;
} }
setTimeout(QR.cooldown.count, $.SECOND); clearTimeout(QR.cooldown.timeout);
QR.cooldown.timeout = setTimeout(QR.cooldown.count, $.SECOND);
now = Date.now(); now = Date.now();
post = QR.posts[0]; post = QR.posts[0];
isReply = post.thread !== 'new'; isReply = post.thread !== 'new';
isSage = /sage/i.test(post.email);
hasFile = !!post.file; hasFile = !!post.file;
seconds = null; seconds = null;
_ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns, upSpd = _ref.upSpd, upSpdAccuracy = _ref.upSpdAccuracy; _ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns, upSpd = _ref.upSpd, upSpdAccuracy = _ref.upSpdAccuracy;
@ -7722,20 +7668,39 @@
} }
continue; continue;
} }
if (isReply === cooldown.isReply) { if ('timeout' in cooldown) {
type = !isReply ? 'thread' : isSage && cooldown.isSage ? 'sage' : hasFile && cooldown.hasFile ? 'file' : 'post';
elapsed = Math.floor((now - start) / $.SECOND);
if (elapsed >= 0) {
seconds = Math.max(seconds, types[type] - elapsed);
if (Conf['Cooldown Prediction'] && hasFile && upSpd) {
seconds -= Math.floor(post.file.size / upSpd * upSpdAccuracy);
seconds = Math.max(seconds, 0);
}
}
}
if (!((start <= now && now <= cooldown.timeout))) {
QR.cooldown.unset(start); QR.cooldown.unset(start);
continue;
} }
if (isReply === cooldown.isReply) {
elapsed = Math.floor((now - start) / $.SECOND);
if (elapsed < 0) {
continue;
}
if (!isReply) {
type = 'thread';
} else if (hasFile) {
if (!cooldown.hasFile) {
seconds = Math.max(seconds, 0);
continue;
}
type = 'image';
} else {
type = 'reply';
}
maxTimer = Math.max(types[type] || 0, types[type + '_intra'] || 0);
if (!((start <= now && now <= start + maxTimer * $.SECOND))) {
QR.cooldown.unset(start);
}
if (isReply && +post.thread === cooldown.threadID) {
type += '_intra';
}
seconds = Math.max(seconds, types[type] - elapsed);
}
}
if (seconds && Conf['Cooldown Prediction'] && hasFile && upSpd) {
seconds -= Math.floor(post.file.size / upSpd * upSpdAccuracy);
seconds = seconds > 0 ? seconds : 0;
} }
update = seconds !== null || !!QR.cooldown.seconds; update = seconds !== null || !!QR.cooldown.seconds;
QR.cooldown.seconds = seconds; QR.cooldown.seconds = seconds;
@ -7948,6 +7913,9 @@
_this.name = 'name' in QR.persona.always ? QR.persona.always.name : prev ? prev.name : persona.name; _this.name = 'name' in QR.persona.always ? QR.persona.always.name : prev ? prev.name : persona.name;
_this.email = 'email' in QR.persona.always ? QR.persona.always.email : prev && !/^sage$/.test(prev.email) ? prev.email : persona.email; _this.email = 'email' in QR.persona.always ? QR.persona.always.email : prev && !/^sage$/.test(prev.email) ? prev.email : persona.email;
_this.sub = 'sub' in QR.persona.always ? QR.persona.always.sub : Conf['Remember Subject'] ? prev ? prev.sub : persona.sub : ''; _this.sub = 'sub' in QR.persona.always ? QR.persona.always.sub : Conf['Remember Subject'] ? prev ? prev.sub : persona.sub : '';
if (QR.nodes.flag) {
_this.flag = prev ? prev.flag : persona.flag;
}
if (QR.selected === _this) { if (QR.selected === _this) {
return _this.load(); return _this.load();
} }
@ -7979,7 +7947,7 @@
}; };
_Class.prototype.lock = function(lock) { _Class.prototype.lock = function(lock) {
var name, _i, _len, _ref; var name, node, _i, _len, _ref;
if (lock == null) { if (lock == null) {
lock = true; lock = true;
@ -7988,10 +7956,12 @@
if (this !== QR.selected) { if (this !== QR.selected) {
return; return;
} }
_ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler']; _ref = ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i]; name = _ref[_i];
QR.nodes[name].disabled = lock; if (node = QR.nodes[name]) {
node.disabled = lock;
}
} }
this.nodes.rm.style.visibility = lock ? 'hidden' : ''; this.nodes.rm.style.visibility = lock ? 'hidden' : '';
(lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput); (lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput);
@ -8021,12 +7991,15 @@
}; };
_Class.prototype.load = function() { _Class.prototype.load = function() {
var name, _i, _len, _ref; var name, node, _i, _len, _ref;
_ref = ['thread', 'name', 'email', 'sub', 'com', 'filename']; _ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i]; name = _ref[_i];
QR.nodes[name].value = this[name] || null; if (!(node = QR.nodes[name])) {
continue;
}
node.value = this[name] || node.dataset["default"] || null;
} }
QR.tripcodeHider.call(QR.nodes['name']); QR.tripcodeHider.call(QR.nodes['name']);
this.showFileData(); this.showFileData();
@ -8041,7 +8014,7 @@
return; return;
} }
name = input.dataset.name; name = input.dataset.name;
this[name] = input.value; this[name] = input.value || input.dataset["default"] || null;
switch (name) { switch (name) {
case 'thread': case 'thread':
return QR.status(); return QR.status();
@ -8065,15 +8038,18 @@
}; };
_Class.prototype.forceSave = function() { _Class.prototype.forceSave = function() {
var name, _i, _len, _ref; var name, node, _i, _len, _ref;
if (this !== QR.selected) { if (this !== QR.selected) {
return; return;
} }
_ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler']; _ref = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler', 'flag'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i]; name = _ref[_i];
this.save(QR.nodes[name]); if (!(node = QR.nodes[name])) {
continue;
}
this.save(node);
} }
}; };
@ -8409,7 +8385,7 @@
} }
}, },
dialog: function() { dialog: function() {
var check, dialog, i, items, key, mimeTypes, name, nodes, thread, value, _ref; var check, dialog, event, flagSelector, i, items, key, mimeTypes, name, node, nodes, save, thread, value, _ref;
QR.nodes = nodes = { QR.nodes = nodes = {
el: dialog = UI.dialog('qr', 'top:0;right:0;', "<div id=qrtab class=move><input type=checkbox id=autohide title=Auto-hide><div id=qr-thread-select><select data-name=thread title='Create a new thread / Reply'><option value=new>New thread</option></select></div><a href=javascript:; class=close title=Close>✖</a></div><form><div class=persona><input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1 tabindex=10><input name=email data-name=email list=\"list-email\" placeholder=E-mail class=field size=1 tabindex=20><input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1 tabindex=30></div><div class=textarea><textarea data-name=com placeholder=Comment class=field tabindex=40></textarea><span id=char-count></span></div><div id=dump-list-container><div id=dump-list></div><a id=add-post href=javascript:; title=\"Add a post\" tabindex=50>+</a></div><div id=file-n-submit><span id=qr-filename-container class=field tabindex=60><span id=qr-no-file>No selected file</span><input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\"><span id=qr-extras-container><label id=qr-spoiler-label><input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=70></label><span class=description>Spoiler</span><a id=dump-button title='Dump list'>+</a><span class=description>Dump</span><a id=qr-filerm href=javascript:; title='Remove file'>✖</a><span class=description>Remove File</span></span></span><input type=submit tabindex=80></div><input type=file multiple></form><datalist id=\"list-name\"></datalist><datalist id=\"list-email\"></datalist><datalist id=\"list-sub\"></datalist>") el: dialog = UI.dialog('qr', 'top:0;right:0;', "<div id=qrtab class=move><input type=checkbox id=autohide title=Auto-hide><div id=qr-thread-select><select data-name=thread title='Create a new thread / Reply'><option value=new>New thread</option></select></div><a href=javascript:; class=close title=Close>✖</a></div><form><div class=persona><input name=name data-name=name list=\"list-name\" placeholder=Name class=field size=1 tabindex=10><input name=email data-name=email list=\"list-email\" placeholder=E-mail class=field size=1 tabindex=20><input name=sub data-name=sub list=\"list-sub\" placeholder=Subject class=field size=1 tabindex=30></div><div class=textarea><textarea data-name=com placeholder=Comment class=field tabindex=40></textarea><span id=char-count></span></div><div id=dump-list-container><div id=dump-list></div><a id=add-post href=javascript:; title=\"Add a post\" tabindex=50>+</a></div><div id=file-n-submit><span id=qr-filename-container class=field tabindex=60><span id=qr-no-file>No selected file</span><input id=\"qr-filename\" data-name=\"filename\" spellcheck=\"false\"><span id=qr-extras-container><label id=qr-spoiler-label><input type=checkbox id=qr-file-spoiler title='Spoiler image' tabindex=70></label><span class=description>Spoiler</span><a id=dump-button title='Dump list'>+</a><span class=description>Dump</span><a id=qr-filerm href=javascript:; title='Remove file'>✖</a><span class=description>Remove File</span></span></span><input type=submit tabindex=80></div><input type=file multiple></form><datalist id=\"list-name\"></datalist><datalist id=\"list-email\"></datalist><datalist id=\"list-sub\"></datalist>")
@ -8469,8 +8445,15 @@
name: 'filetag', 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>" 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>"
}); });
nodes.flashTag.dataset["default"] = '4';
$.add(nodes.form, nodes.flashTag); $.add(nodes.form, nodes.flashTag);
} }
if (flagSelector = $('.flagSelector')) {
nodes.flag = flagSelector.cloneNode(true);
nodes.flag.dataset.name = 'flag';
nodes.flag.dataset["default"] = '0';
$.add(nodes.form, nodes.flag);
}
for (thread in g.BOARD.threads) { for (thread in g.BOARD.threads) {
$.add(nodes.thread, $.el('option', { $.add(nodes.thread, $.el('option', {
value: thread, value: thread,
@ -8507,12 +8490,17 @@
while (name = items[i++]) { while (name = items[i++]) {
$.on(nodes[name], 'mouseover', QR.mouseover); $.on(nodes[name], 'mouseover', QR.mouseover);
} }
items = ['name', 'email', 'sub', 'com', 'filename']; items = ['name', 'email', 'sub', 'com', 'filename', 'flag'];
i = 0; i = 0;
save = function() {
return QR.selected.save(this);
};
while (name = items[i++]) { while (name = items[i++]) {
$.on(nodes[name], 'input', function() { if (!(node = nodes[name])) {
return QR.selected.save(this); continue;
}); }
event = node.nodeName === 'SELECT' ? 'change' : 'input';
$.on(nodes[name], event, save);
} }
$.on(nodes['name'], 'blur', QR.tripcodeHider); $.on(nodes['name'], 'blur', QR.tripcodeHider);
$.on(nodes.thread, 'change', function() { $.on(nodes.thread, 'change', function() {
@ -8542,7 +8530,7 @@
}, },
preSubmitHooks: [], preSubmitHooks: [],
submit: function(e) { submit: function(e) {
var challenge, err, extra, filetag, hook, options, post, postData, response, textOnly, thread, threadID, _i, _len, _ref, _ref1; var challenge, err, extra, filetag, formData, hook, options, post, response, textOnly, thread, threadID, _i, _len, _ref, _ref1;
if (e != null) { if (e != null) {
e.preventDefault(); e.preventDefault();
@ -8565,7 +8553,7 @@
thread = g.BOARD.threads[threadID]; thread = g.BOARD.threads[threadID];
if (threadID === 'new') { if (threadID === 'new') {
threadID = null; threadID = null;
if (['vg', 'q'].contains(g.BOARD.ID) && !post.sub) { if (g.BOARD.ID === 'vg' && !post.sub) {
err = 'New threads require a subject.'; err = 'New threads require a subject.';
} else if (!(post.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) { } else if (!(post.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) {
err = 'No file selected.'; err = 'No file selected.';
@ -8606,7 +8594,7 @@
d.activeElement.blur(); d.activeElement.blur();
} }
post.lock(); post.lock();
postData = { formData = {
resto: threadID, resto: threadID,
name: post.name, name: post.name,
email: post.email, email: post.email,
@ -8615,6 +8603,7 @@
upfile: post.file, upfile: post.file,
filetag: filetag, filetag: filetag,
spoiler: post.spoiler, spoiler: post.spoiler,
flag: post.flag,
textonly: textOnly, textonly: textOnly,
mode: 'regist', mode: 'regist',
pwd: QR.persona.pwd, pwd: QR.persona.pwd,
@ -8636,7 +8625,7 @@
} }
}; };
extra = { extra = {
form: $.formData(postData), form: $.formData(formData),
upCallbacks: { upCallbacks: {
onload: function() { onload: function() {
QR.req.isUploadFinished = true; QR.req.isUploadFinished = true;
@ -8720,8 +8709,8 @@
threadID: threadID, threadID: threadID,
postID: postID postID: postID
}); });
postsCount = QR.posts.length; postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount > 1 && isReply; QR.cooldown.auto = postsCount && isReply;
if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) { if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
notif = new Notification('Quick reply warning', { notif = new Notification('Quick reply warning', {
body: "You are running low on cached captchas. Cache count: " + captchasCount + ".", body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
@ -8746,7 +8735,8 @@
QR.cooldown.set({ QR.cooldown.set({
req: req, req: req,
post: post, post: post,
isReply: isReply isReply: isReply,
threadID: threadID
}); });
URL = !isReply ? "/" + g.BOARD + "/res/" + threadID : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? "/" + g.BOARD + "/res/" + threadID + "#p" + postID : void 0; URL = !isReply ? "/" + g.BOARD + "/res/" + threadID : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? "/" + g.BOARD + "/res/" + threadID + "#p" + postID : void 0;
if (URL) { if (URL) {
@ -9777,7 +9767,7 @@
open: function(post) { open: function(post) {
var node; var node;
if (post.isDead || post.board.ID === 'q') { if (post.isDead) {
return false; return false;
} }
DeleteLink.post = post; DeleteLink.post = post;
@ -9852,7 +9842,7 @@
return; return;
} }
DeleteLink.cooldown.counting = post; DeleteLink.cooldown.counting = post;
length = 30; length = 60;
seconds = Math.ceil((length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND); seconds = Math.ceil((length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND);
return DeleteLink.cooldown.count(post, seconds, length, node); return DeleteLink.cooldown.count(post, seconds, length, node);
}, },
@ -10148,7 +10138,6 @@
if (g.VIEW !== 'thread' || !Conf['Thread Updater']) { if (g.VIEW !== 'thread' || !Conf['Thread Updater']) {
return; return;
} }
checked = Conf['Auto Update'] ? 'checked' : '';
if (Conf['Updater and Stats in Header']) { if (Conf['Updater and Stats in Header']) {
this.dialog = sc = $.el('span', { this.dialog = sc = $.el('span', {
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0", innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0",
@ -10168,6 +10157,7 @@
this.checkPostCount = 0; this.checkPostCount = 0;
this.timer = $('#update-timer', sc); this.timer = $('#update-timer', sc);
this.status = $('#update-status', sc); this.status = $('#update-status', sc);
this.isUpdating = Conf['Auto Update'];
$.on(this.timer, 'click', ThreadUpdater.update); $.on(this.timer, 'click', ThreadUpdater.update);
$.on(this.status, 'click', ThreadUpdater.update); $.on(this.status, 'click', ThreadUpdater.update);
subEntries = []; subEntries = [];
@ -10185,7 +10175,7 @@
$.on(input, 'change', ThreadUpdater.cb.scrollBG); $.on(input, 'change', ThreadUpdater.cb.scrollBG);
ThreadUpdater.cb.scrollBG(); ThreadUpdater.cb.scrollBG();
} else if (input.name === 'Auto Update') { } else if (input.name === 'Auto Update') {
$.on(input, 'change', ThreadUpdater.update); $.on(input, 'change', ThreadUpdater.cb.update);
} }
subEntries.push({ subEntries.push({
el: el el: el
@ -10215,7 +10205,6 @@
ThreadUpdater.thread = this; ThreadUpdater.thread = this;
ThreadUpdater.root = this.OP.nodes.root.parentNode; ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]; ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0];
ThreadUpdater.outdateCount = 0;
ThreadUpdater.cb.interval.call($.el('input', { ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval'] value: Conf['Interval']
})); }));
@ -10233,19 +10222,18 @@
beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA', beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA',
cb: { cb: {
online: function() { online: function() {
if (ThreadUpdater.online = navigator.onLine) { if (navigator.onLine) {
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
ThreadUpdater.setInterval(); ThreadUpdater.setInterval();
ThreadUpdater.update();
ThreadUpdater.set('status', null, null); ThreadUpdater.set('status', null, null);
} else { } else {
ThreadUpdater.set('timer', null); ThreadUpdater.set('timer', null);
ThreadUpdater.set('status', 'Offline', 'warning'); ThreadUpdater.set('status', 'Offline', 'warning');
} }
return ThreadUpdater.cb.autoUpdate(); return ThreadUpdater.count(true);
}, },
post: function(e) { post: function(e) {
if (e.detail.threadID !== ThreadUpdater.thread.ID) { if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) {
return; return;
} }
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
@ -10275,9 +10263,7 @@
return; return;
} }
ThreadUpdater.outdateCount = 0; ThreadUpdater.outdateCount = 0;
if (ThreadUpdater.seconds > ThreadUpdater.interval) { return ThreadUpdater.seconds = Math.min(ThreadUpdater.seconds, ThreadUpdater.interval);
return ThreadUpdater.setInterval();
}
}, },
scrollBG: function() { scrollBG: function() {
return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? function() { return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? function() {
@ -10286,17 +10272,10 @@
return !d.hidden; return !d.hidden;
}; };
}, },
autoUpdate: function() {
if (ThreadUpdater.online) {
return ThreadUpdater.timeout();
} else {
return clearTimeout(ThreadUpdater.timeoutID);
}
},
interval: function() { interval: function() {
var val; var val;
val = +this.value; val = parseInt(this.value, 10);
if (val < 1) { if (val < 1) {
val = 1; val = 1;
} }
@ -10307,9 +10286,9 @@
var klass, req, text, _ref; var klass, req, text, _ref;
req = ThreadUpdater.req; req = ThreadUpdater.req;
delete ThreadUpdater.req;
if (e.type !== 'loadend') { if (e.type !== 'loadend') {
req.onloadend = null; req.onloadend = null;
delete ThreadUpdater.req;
if (e.type === 'timeout') { if (e.type === 'timeout') {
ThreadUpdater.set('status', 'Retrying', null); ThreadUpdater.set('status', 'Retrying', null);
ThreadUpdater.update(); ThreadUpdater.update();
@ -10339,9 +10318,8 @@
ThreadUpdater.set('status', text, klass); ThreadUpdater.set('status', text, klass);
} }
if (ThreadUpdater.postID) { if (ThreadUpdater.postID) {
ThreadUpdater.cb.checkpost(); return ThreadUpdater.cb.checkpost();
} }
return delete ThreadUpdater.req;
} }
}, },
setInterval: function() { setInterval: function() {
@ -10354,8 +10332,7 @@
} }
ThreadUpdater.seconds = Conf['Optional Increase'] ? (cur = [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] > i) ? cur : i : i; ThreadUpdater.seconds = Conf['Optional Increase'] ? (cur = [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] > i) ? cur : i : i;
ThreadUpdater.set('timer', ThreadUpdater.seconds++); ThreadUpdater.set('timer', ThreadUpdater.seconds++);
clearTimeout(ThreadUpdater.timeoutID); return ThreadUpdater.count(true);
return ThreadUpdater.timeout();
}, },
intervalShortcut: function() { intervalShortcut: function() {
var settings; var settings;
@ -10377,20 +10354,29 @@
return el.className = klass; return el.className = klass;
} }
}, },
count: function(start) {
clearTimeout(ThreadUpdater.timeoutID);
if (start && ThreadUpdater.isUpdating && navigator.onLine) {
return ThreadUpdater.timeout();
}
},
timeout: function() { timeout: function() {
var sec;
ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000);
ThreadUpdater.set('timer', --ThreadUpdater.seconds); sec = ThreadUpdater.seconds--;
if (ThreadUpdater.seconds <= 0) { ThreadUpdater.set('timer', sec);
if (sec <= 0) {
return ThreadUpdater.update(); return ThreadUpdater.update();
} }
}, },
update: function() { update: function() {
var url; var url;
if (!ThreadUpdater.online) { if (!navigator.onLine) {
return; return;
} }
clearTimeout(ThreadUpdater.timeoutID); ThreadUpdater.count();
if (Conf['Auto Update']) { if (Conf['Auto Update']) {
ThreadUpdater.set('timer', '...'); ThreadUpdater.set('timer', '...');
} else { } else {
@ -10564,27 +10550,6 @@
ThreadWatcher.fetchAllStatus(); ThreadWatcher.fetchAllStatus();
this.db.save(); this.db.save();
} }
$.get('WatchedThreads', null, function(_arg) {
var WatchedThreads, boardID, data, threadID, threads, _ref;
WatchedThreads = _arg.WatchedThreads;
if (!WatchedThreads) {
return;
}
_ref = ThreadWatcher.convert(WatchedThreads);
for (boardID in _ref) {
threads = _ref[boardID];
for (threadID in threads) {
data = threads[threadID];
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
}
}
return $["delete"]('WatchedThreads');
});
return Thread.prototype.callbacks.push({ return Thread.prototype.callbacks.push({
name: 'Thread Watcher', name: 'Thread Watcher',
cb: this.node cb: this.node
@ -11371,6 +11336,7 @@
'4plebs': { '4plebs': {
domain: 'archive.4plebs.org', domain: 'archive.4plebs.org',
http: true, http: true,
https: true,
software: 'foolfuuka', software: 'foolfuuka',
boards: ['hr', 'tg', 'tv', 'x'], boards: ['hr', 'tg', 'tv', 'x'],
files: ['hr', 'tg', 'tv', 'x'] files: ['hr', 'tg', 'tv', 'x']
@ -11378,10 +11344,10 @@
'fap archive': { 'fap archive': {
domain: 'fuuka.worldathleticproject.org', domain: 'fuuka.worldathleticproject.org',
http: true, http: true,
https: false, https: true,
software: 'foolfuuka', software: 'foolfuuka',
boards: ['b', 'e', 'h', 'hc', 'p', 's', 'u'], boards: ['b', 'e', 'h', 'hc', 'p', 's', 'u'],
files: ['b', 'e', 'h', 'hc', 'p', 's', 'u'] files: ['b', 'e', 'h', 'hc', 'p', 's', 'soc', 'sp', 'u']
}, },
'Foolz': { 'Foolz': {
domain: 'archive.foolz.us', domain: 'archive.foolz.us',
@ -11405,8 +11371,8 @@
https: true, https: true,
withCredentials: true, withCredentials: true,
software: 'foolfuuka', software: 'foolfuuka',
boards: ['a', 'co', 'gd', 'h', 'jp', 'm', 'mlp', 'q', 'sp', 'tg', 'tv', 'u', 'v', 'vg', 'vp', 'vr', 'wsg'], boards: ['a', 'co', 'd', 'gd', 'h', 'jp', 'm', 'mlp', 'q', 'sp', 'tg', 'tv', 'u', 'v', 'vg', 'vp', 'vr', 'wsg'],
files: ['a', 'gd', 'h', 'jp', 'm', 'q', 'tg', 'u', 'vg', 'vp', 'vr', 'wsg'] files: ['a', 'd', 'gd', 'h', 'jp', 'm', 'q', 'tg', 'u', 'vg', 'vp', 'vr', 'wsg']
}, },
'Heinessen': { 'Heinessen': {
domain: 'archive.heinessen.com', domain: 'archive.heinessen.com',
@ -11496,14 +11462,10 @@
return "" + (Redirect.protocol(archive)) + archive.domain + "/" + path; return "" + (Redirect.protocol(archive)) + archive.domain + "/" + path;
}, },
post: function(archive, _arg) { post: function(archive, _arg) {
var URL, boardID, postID, protocol; var URL, boardID, postID;
boardID = _arg.boardID, postID = _arg.postID; boardID = _arg.boardID, postID = _arg.postID;
protocol = Redirect.protocol(archive); URL = new String("" + (Redirect.protocol(archive)) + archive.domain + "/_/api/chan/post/?board=" + boardID + "&num=" + postID);
if (['Foolz', 'NSFW Foolz'].contains(archive.name)) {
protocol = 'https://';
}
URL = new String("" + protocol + archive.domain + "/_/api/chan/post/?board=" + boardID + "&num=" + postID);
URL.archive = archive; URL.archive = archive;
return URL; return URL;
}, },
@ -13414,12 +13376,6 @@
} }
quote.href = "/" + post.board + "/res/" + href; quote.href = "/" + post.board + "/res/" + href;
} }
Build.capcodeReplies({
boardID: post.board.ID,
threadID: post.thread.ID,
bq: clone,
capcodeReplies: postObj.capcode_replies
});
post.nodes.shortComment = comment; post.nodes.shortComment = comment;
$.replace(comment, clone); $.replace(comment, clone);
post.nodes.comment = post.nodes.longComment = clone; post.nodes.comment = post.nodes.longComment = clone;
@ -13505,7 +13461,6 @@
switch (g.BOARD.ID) { switch (g.BOARD.ID) {
case 'b': case 'b':
case 'vg': case 'vg':
case 'q':
return 3; return 3;
case 't': case 't':
return 1; return 1;

View File

@ -581,7 +581,7 @@ a.hide-announcement {
align-self: stretch; align-self: stretch;
flex: 1; flex: 1;
} }
#qr select { #qr select[data-name=thread] {
margin: 0; margin: 0;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
@ -794,7 +794,6 @@ a.hide-announcement {
} }
#file-n-submit-container input { #file-n-submit-container input {
margin: 0; margin: 0;
padding: 0;
} }
#file-n-submit input[type='submit'] { #file-n-submit input[type='submit'] {
order: 1; order: 1;

110
json/archives.json Normal file
View File

@ -0,0 +1,110 @@
[{
"uid": 0,
"name": "Foolz",
"domain": "archive.foolz.us",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["a", "co", "gd", "jp", "m", "q", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "gd", "jp", "m", "q", "tg", "vg", "vp", "vr", "wsg"]
}, {
"uid": 1,
"name": "NSFW Foolz",
"domain": "nsfw.foolz.us",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["u"],
"files": ["u"]
}, {
"uid": 2,
"name": "The Dark Cave",
"domain": "archive.thedarkcave.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["c", "int", "out", "po"],
"files": ["c", "po"]
}, {
"uid": 3,
"name": "4plebs",
"domain": "archive.4plebs.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["hr", "tg", "tv", "x"],
"files": ["hr", "tg", "tv", "x"]
}, {
"uid": 4,
"name": "Nyafuu",
"domain": "archive.nyafuu.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["c", "w", "wg"],
"files": ["c", "w", "wg"]
}, {
"uid": 11,
"name": "Foolz a Shit",
"domain": "archive.foolzashit.com",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["adv", "asp", "cm", "d", "e", "i", "lgbt", "n", "o", "p", "pol", "s", "s4s", "t", "trv", "y"],
"files": ["cm", "d", "e", "i", "n", "o", "p", "s", "trv", "y"]
}, {
"uid": 12,
"name": "FapArchive",
"domain": "fuuka.worldathleticproject.org",
"http": true,
"https": true,
"software": "foolfuuka",
"boards": ["b", "e", "h", "hc", "p", "s", "soc", "sp", "u"],
"files": ["b", "e", "h", "hc", "p", "s", "soc", "sp", "u"]
}, {
"uid": 7,
"name": "Install Gentoo",
"domain": "archive.installgentoo.net",
"http": false,
"https": true,
"software": "fuuka",
"boards": ["diy", "g", "sci"],
"files": []
}, {
"uid": 8,
"name": "Rebecca Black Tech",
"domain": "rbt.asia",
"http": true,
"https": true,
"software": "fuuka",
"boards": ["cgl", "g", "mu", "w"],
"files": ["cgl", "g", "mu", "w"]
}, {
"uid": 9,
"name": "Heinessen",
"domain": "archive.heinessen.com",
"http": true,
"https": false,
"software": "fuuka",
"boards": ["an", "fit", "k", "mlp", "r9k", "toy"],
"files": ["an", "fit", "k", "r9k", "toy"]
}, {
"uid": 10,
"name": "warosu",
"domain": "fuuka.warosu.org",
"http": true,
"https": true,
"software": "fuuka",
"boards": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "q", "tg", "vr"],
"files": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "q", "tg", "vr"]
}, {
"uid": 13,
"name": "Foolz Beta",
"domain": "beta.foolz.us",
"http": true,
"https": true,
"withCredentials": true,
"software": "foolfuuka",
"boards": ["a", "co", "d", "gd", "h", "jp", "m", "mlp", "q", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "d", "gd", "h", "jp", "m", "q", "tg", "u", "vg", "vp", "vr", "wsg"]
}]

View File

@ -22,14 +22,14 @@
"font-awesome": "git://github.com/MayhemYDG/Font-Awesome.git#df4285951124f9ca1f3907438462e5ba9e464bcb", "font-awesome": "git://github.com/MayhemYDG/Font-Awesome.git#df4285951124f9ca1f3907438462e5ba9e464bcb",
"grunt": "~0.4.1", "grunt": "~0.4.1",
"grunt-bump": "~0.0.11", "grunt-bump": "~0.0.11",
"grunt-concurrent": "~0.3.0", "grunt-concurrent": "~0.3.1",
"grunt-contrib-clean": "~0.5.0", "grunt-contrib-clean": "~0.5.0",
"grunt-contrib-coffee": "~0.7.0", "grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-compress": "~0.5.2", "grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "~0.3.0", "grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.4.1", "grunt-contrib-copy": "~0.4.1",
"grunt-contrib-watch": "~0.5.0", "grunt-contrib-watch": "~0.5.3",
"grunt-shell": "~0.3.1", "grunt-shell": "~0.4.0",
"load-grunt-tasks": "~0.1.0" "load-grunt-tasks": "~0.1.0"
}, },
"repository": { "repository": {

View File

@ -24,7 +24,8 @@ Redirect =
archives: archives:
'4plebs': '4plebs':
domain: 'archive.4plebs.org' domain: 'archive.4plebs.org'
http: true http: true
https: true
software: 'foolfuuka' software: 'foolfuuka'
boards: ['hr', 'tg', 'tv', 'x'] boards: ['hr', 'tg', 'tv', 'x']
files: ['hr', 'tg', 'tv', 'x'] files: ['hr', 'tg', 'tv', 'x']
@ -32,10 +33,10 @@ Redirect =
'fap archive': 'fap archive':
domain: 'fuuka.worldathleticproject.org' domain: 'fuuka.worldathleticproject.org'
http: true http: true
https: false https: true
software: 'foolfuuka' software: 'foolfuuka'
boards: ['b', 'e', 'h', 'hc', 'p', 's', 'u'] boards: ['b', 'e', 'h', 'hc', 'p', 's', 'u']
files: ['b', 'e', 'h', 'hc', 'p', 's', 'u'] files: ['b', 'e', 'h', 'hc', 'p', 's', 'soc', 'sp', 'u']
'Foolz': 'Foolz':
domain: 'archive.foolz.us' domain: 'archive.foolz.us'
@ -59,8 +60,8 @@ Redirect =
https: true https: true
withCredentials: true withCredentials: true
software: 'foolfuuka' software: 'foolfuuka'
boards: ['a', 'co', 'gd', 'h', 'jp', 'm', 'mlp', 'q', 'sp', 'tg', 'tv', 'u', 'v', 'vg', 'vp', 'vr', 'wsg'], boards: ['a', 'co', 'd', 'gd', 'h', 'jp', 'm', 'mlp', 'q', 'sp', 'tg', 'tv', 'u', 'v', 'vg', 'vp', 'vr', 'wsg'],
files: ['a', 'gd', 'h', 'jp', 'm', 'q', 'tg', 'u', 'vg', 'vp', 'vr', 'wsg'] files: ['a', 'd', 'gd', 'h', 'jp', 'm', 'q', 'tg', 'u', 'vg', 'vp', 'vr', 'wsg']
'Heinessen': 'Heinessen':
domain: 'archive.heinessen.com' domain: 'archive.heinessen.com'
@ -146,12 +147,7 @@ Redirect =
post: (archive, {boardID, postID}) -> post: (archive, {boardID, postID}) ->
# For fuuka-based archives: # For fuuka-based archives:
# https://github.com/eksopl/fuuka/issues/27 # https://github.com/eksopl/fuuka/issues/27
protocol = Redirect.protocol archive URL = new String "#{Redirect.protocol archive}#{archive.domain}/_/api/chan/post/?board=#{boardID}&num=#{postID}"
# XXX foolz had HSTS set for 120 days, which broke XHR+CORS+Redirection when on HTTP.
# Remove necessary HTTPS procotol in September 2013.
if ['Foolz', 'NSFW Foolz'].contains archive.name
protocol = 'https://'
URL = new String "#{protocol}#{archive.domain}/_/api/chan/post/?board=#{boardID}&num=#{postID}"
URL.archive = archive URL.archive = archive
URL URL

View File

@ -14,8 +14,5 @@ Anonymize =
$.rm tripcode $.rm tripcode
delete @nodes.tripcode delete @nodes.tripcode
if @info.email if @info.email
if /sage/i.test @info.email $.replace email, name
email.href = 'mailto:sage' delete @nodes.email
else
$.replace email, name
delete @nodes.email

View File

@ -184,7 +184,7 @@ PostHiding =
if Conf['Anonymize'] if Conf['Anonymize']
'Anonymous' 'Anonymous'
else else
post.info.name $('.nameBlock', post.nodes.info).textContent
$.add a, $.tn " #{postInfo}" $.add a, $.tn " #{postInfo}"
post.nodes.stub = $.el 'div', post.nodes.stub = $.el 'div',
className: 'stub' className: 'stub'

View File

@ -185,7 +185,7 @@ ThreadHiding =
if Conf['Anonymize'] if Conf['Anonymize']
'Anonymous' 'Anonymous'
else else
OP.info.name $('.nameBlock', OP.nodes.info).textContent
a = ThreadHiding.makeButton thread, 'show' a = ThreadHiding.makeButton thread, 'show'
$.add a, $.tn " #{opInfo} (#{numReplies})" $.add a, $.tn " #{opInfo} (#{numReplies})"

View File

@ -27,7 +27,6 @@ Build =
date: data.now date: data.now
dateUTC: data.time dateUTC: data.time
comment: data.com comment: data.com
capcodeReplies: data.capcode_replies
# thread status # thread status
isSticky: !!data.sticky isSticky: !!data.sticky
isClosed: !!data.closed isClosed: !!data.closed
@ -59,7 +58,7 @@ Build =
postID, threadID, boardID postID, threadID, boardID
name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC
isSticky, isClosed isSticky, isClosed
comment, capcodeReplies comment
file file
} = o } = o
isOP = postID is threadID isOP = postID is threadID
@ -201,36 +200,4 @@ Build =
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
Build.capcodeReplies {boardID, threadID, root: container, capcodeReplies}
container container
capcodeReplies: ({boardID, threadID, bq, root, capcodeReplies}) ->
return unless capcodeReplies
generateCapcodeReplies = (capcodeType, array) ->
"<span class=smaller><span class=bold>#{
switch capcodeType
when 'admin'
'Administrator'
when 'mod'
'Moderator'
when 'developer'
'Developer'
} Repl#{if array.length > 1 then 'ies' else 'y'}:</span> #{
array.map (ID) ->
"<a href='/#{boardID}/res/#{threadID}#p#{ID}' class=quotelink>&gt;&gt;#{ID}</a>"
.join ' '
}</span><br>"
html = []
for capcodeType, array of capcodeReplies
html.push generateCapcodeReplies capcodeType, array
bq or= $ 'blockquote', root
$.add bq, [
$.el 'br'
$.el 'br'
$.el 'span',
className: 'capcodeReplies'
innerHTML: html.join ''
]

View File

@ -799,12 +799,8 @@ Config =
#/Mod$/;highlight:mod;op:yes #/Mod$/;highlight:mod;op:yes
# Set a custom class for moot: # Set a custom class for moot:
#/Admin$/;highlight:moot;op:yes #/Admin$/;highlight:moot;op:yes
""" """
email: ""
email: """
# Filter any e-mails that are not `sage` on /a/ and /jp/:
#/^(?!sage$)/;boards:a,jp
"""
subject: """ subject: """
# Filter Generals on /v/: # Filter Generals on /v/:
#/general/i;boards:v;op:only #/general/i;boards:v;op:only

1271
src/General/css/style.css Executable file

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,7 @@
<div class='postInfo desktop' id=pi#{postID}> <div class='postInfo desktop' id=pi#{postID}>
<input type=checkbox name=#{postID} value=delete> <input type=checkbox name=#{postID} value=delete>
#{subject}&nbsp; &nbsp;#{subject}&nbsp;
<span class='nameBlock#{capcodeClass}'> <span class='nameBlock#{capcodeClass}'>
#{emailStart} #{emailStart}
<span class=name>#{name or ''}</span> <span class=name>#{name or ''}</span>

View File

@ -68,13 +68,12 @@ class Post
# <br> -> \n # <br> -> \n
# Remove: # Remove:
# 'Comment too long'... # 'Comment too long'...
# Admin/Mod/Dev replies. (/q/)
# EXIF data. (/p/) # EXIF data. (/p/)
# Rolls. (/tg/) # Rolls. (/tg/)
# Preceding and following new lines. # Preceding and following new lines.
# Trailing spaces. # Trailing spaces.
bq = @nodes.comment.cloneNode true bq = @nodes.comment.cloneNode true
nodes = $$ '.abbr, .capcodeReplies, .exif, b', bq nodes = $$ '.abbr, .exif, b', bq
i = 0 i = 0
while node = nodes[i++] while node = nodes[i++]
$.rm node $.rm node
@ -108,8 +107,7 @@ class Post
@nodes.quotelinks.push quotelink @nodes.quotelinks.push quotelink
# Don't count capcode replies as quotes in OPs. (Admin/Mod/Dev Replies: ...) return if @isClone
return if @isClone or !@isReply and $.hasClass quotelink.parentNode.parentNode, 'capcodeReplies'
# ES6 Set when? # ES6 Set when?
fullID = "#{match[1]}.#{match[2]}" fullID = "#{match[1]}.#{match[2]}"

View File

@ -31,7 +31,7 @@ DeleteLink =
el: div el: div
order: 40 order: 40
open: (post) -> open: (post) ->
return false if post.isDead or post.board.ID is 'q' return false if post.isDead
DeleteLink.post = post DeleteLink.post = post
node = div.firstChild node = div.firstChild
node.textContent = 'Delete' node.textContent = 'Delete'
@ -84,7 +84,7 @@ DeleteLink =
delete DeleteLink.cooldown.counting delete DeleteLink.cooldown.counting
return return
DeleteLink.cooldown.counting = post DeleteLink.cooldown.counting = post
length = 30 length = 60
seconds = Math.ceil (length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND seconds = Math.ceil (length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND
DeleteLink.cooldown.count post, seconds, length, node DeleteLink.cooldown.count post, seconds, length, node
count: (post, seconds, length, node) -> count: (post, seconds, length, node) ->

View File

@ -60,11 +60,6 @@ ExpandComment =
href = quote.getAttribute 'href' href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{post.board}/res/#{href}" # Fix pathnames quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
Build.capcodeReplies
boardID: post.board.ID
threadID: post.thread.ID
bq: clone
capcodeReplies: postObj.capcode_replies
post.nodes.shortComment = comment post.nodes.shortComment = comment
$.replace comment, clone $.replace comment, clone
post.nodes.comment = post.nodes.longComment = clone post.nodes.comment = post.nodes.longComment = clone

View File

@ -54,7 +54,7 @@ ExpandThread =
1 1
else switch g.BOARD.ID else switch g.BOARD.ID
# XXX boards config # XXX boards config
when 'b', 'vg', 'q' then 3 when 'b', 'vg' then 3
when 't' then 1 when 't' then 1
else 5 else 5
posts = $$ ".thread > .replyContainer", threadRoot posts = $$ ".thread > .replyContainer", threadRoot

View File

@ -2,8 +2,6 @@ ThreadUpdater =
init: -> init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
checked = if Conf['Auto Update'] then 'checked' else ''
if Conf['Updater and Stats in Header'] if Conf['Updater and Stats in Header']
@dialog = sc = $.el 'span', @dialog = sc = $.el 'span',
innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0" innerHTML: "[<span id=update-status></span><span id=update-timer title='Update now'></span>]\u00A0"
@ -16,12 +14,13 @@ ThreadUpdater =
$.addClass doc, 'float' $.addClass doc, 'float'
$.ready => $.ready =>
$.addClass doc, 'float' $.addClass doc, 'float'
$.add d.body, sc $.add d.body, sc
@checkPostCount = 0 @checkPostCount = 0
@timer = $ '#update-timer', sc @timer = $ '#update-timer', sc
@status = $ '#update-status', sc @status = $ '#update-status', sc
@isUpdating = Conf['Auto Update']
$.on @timer, 'click', ThreadUpdater.update $.on @timer, 'click', ThreadUpdater.update
$.on @status, 'click', ThreadUpdater.update $.on @status, 'click', ThreadUpdater.update
@ -38,7 +37,7 @@ ThreadUpdater =
$.on input, 'change', ThreadUpdater.cb.scrollBG $.on input, 'change', ThreadUpdater.cb.scrollBG
ThreadUpdater.cb.scrollBG() ThreadUpdater.cb.scrollBG()
else if input.name is 'Auto Update' else if input.name is 'Auto Update'
$.on input, 'change', ThreadUpdater.update $.on input, 'change', ThreadUpdater.cb.update
subEntries.push el: el subEntries.push el: el
settings = $.el 'span', settings = $.el 'span',
@ -63,7 +62,6 @@ ThreadUpdater =
ThreadUpdater.thread = @ ThreadUpdater.thread = @
ThreadUpdater.root = @OP.nodes.root.parentNode ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0] ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]
ThreadUpdater.outdateCount = 0
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval'] ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
@ -82,17 +80,16 @@ ThreadUpdater =
cb: cb:
online: -> online: ->
if ThreadUpdater.online = navigator.onLine if navigator.onLine
ThreadUpdater.outdateCount = 0 ThreadUpdater.outdateCount = 0
ThreadUpdater.setInterval() ThreadUpdater.setInterval()
ThreadUpdater.update()
ThreadUpdater.set 'status', null, null ThreadUpdater.set 'status', null, null
else else
ThreadUpdater.set 'timer', null ThreadUpdater.set 'timer', null
ThreadUpdater.set 'status', 'Offline', 'warning' ThreadUpdater.set 'status', 'Offline', 'warning'
ThreadUpdater.cb.autoUpdate() ThreadUpdater.count true
post: (e) -> post: (e) ->
return unless e.detail.threadID is ThreadUpdater.thread.ID return unless ThreadUpdater.isUpdating and e.detail.threadID is ThreadUpdater.thread.ID
ThreadUpdater.outdateCount = 0 ThreadUpdater.outdateCount = 0
setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2 setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2
checkpost: (e) -> checkpost: (e) ->
@ -111,28 +108,22 @@ ThreadUpdater =
return if d.hidden return if d.hidden
# Reset the counter when we focus this tab. # Reset the counter when we focus this tab.
ThreadUpdater.outdateCount = 0 ThreadUpdater.outdateCount = 0
if ThreadUpdater.seconds > ThreadUpdater.interval ThreadUpdater.seconds = Math.min ThreadUpdater.seconds, ThreadUpdater.interval
ThreadUpdater.setInterval()
scrollBG: -> scrollBG: ->
ThreadUpdater.scrollBG = if Conf['Scroll BG'] ThreadUpdater.scrollBG = if Conf['Scroll BG']
-> true -> true
else else
-> not d.hidden -> not d.hidden
autoUpdate: ->
if ThreadUpdater.online
ThreadUpdater.timeout()
else
clearTimeout ThreadUpdater.timeoutID
interval: -> interval: ->
val = +@value val = parseInt @value, 10
if val < 1 then val = 1 if val < 1 then val = 1
ThreadUpdater.interval = @value = val ThreadUpdater.interval = @value = val
$.cb.value.call @ $.cb.value.call @
load: (e) -> load: (e) ->
{req} = ThreadUpdater {req} = ThreadUpdater
delete ThreadUpdater.req
if e.type isnt 'loadend' # timeout or abort if e.type isnt 'loadend' # timeout or abort
req.onloadend = null req.onloadend = null
delete ThreadUpdater.req
if e.type is 'timeout' if e.type is 'timeout'
ThreadUpdater.set 'status', 'Retrying', null ThreadUpdater.set 'status', 'Retrying', null
ThreadUpdater.update() ThreadUpdater.update()
@ -162,8 +153,6 @@ ThreadUpdater =
if ThreadUpdater.postID if ThreadUpdater.postID
ThreadUpdater.cb.checkpost() ThreadUpdater.cb.checkpost()
delete ThreadUpdater.req
setInterval: -> setInterval: ->
i = ThreadUpdater.interval i = ThreadUpdater.interval
# Math.min/max is provably slow: http://jsperf.com/math-s-min-max-vs-homemade/5 # Math.min/max is provably slow: http://jsperf.com/math-s-min-max-vs-homemade/5
@ -177,8 +166,7 @@ ThreadUpdater =
else else
i i
ThreadUpdater.set 'timer', ThreadUpdater.seconds++ ThreadUpdater.set 'timer', ThreadUpdater.seconds++
clearTimeout ThreadUpdater.timeoutID ThreadUpdater.count true
ThreadUpdater.timeout()
intervalShortcut: -> intervalShortcut: ->
Settings.open 'Advanced' Settings.open 'Advanced'
@ -195,14 +183,19 @@ ThreadUpdater =
el.textContent = text el.textContent = text
el.className = klass if klass isnt undefined el.className = klass if klass isnt undefined
count: (start) ->
clearTimeout ThreadUpdater.timeoutID
ThreadUpdater.timeout() if start and ThreadUpdater.isUpdating and navigator.onLine
timeout: -> timeout: ->
ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000
ThreadUpdater.set 'timer', --ThreadUpdater.seconds sec = ThreadUpdater.seconds--
ThreadUpdater.update() if ThreadUpdater.seconds <= 0 ThreadUpdater.set 'timer', sec
ThreadUpdater.update() if sec <= 0
update: -> update: ->
return unless ThreadUpdater.online return unless navigator.onLine
clearTimeout ThreadUpdater.timeoutID ThreadUpdater.count()
if Conf['Auto Update'] if Conf['Auto Update']
ThreadUpdater.set 'timer', '...' ThreadUpdater.set 'timer', '...'
else else

View File

@ -19,14 +19,6 @@ ThreadWatcher =
ThreadWatcher.fetchAllStatus() ThreadWatcher.fetchAllStatus()
@db.save() @db.save()
# XXX tmp conversion from old to new format
$.get 'WatchedThreads', null, ({WatchedThreads}) ->
return unless WatchedThreads
for boardID, threads of ThreadWatcher.convert WatchedThreads
for threadID, data of threads
ThreadWatcher.db.set {boardID, threadID, val: data}
$.delete 'WatchedThreads'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Watcher' name: 'Thread Watcher'
cb: @node cb: @node

View File

@ -268,27 +268,26 @@ QR =
name: post.name name: post.name
email: if /^sage$/.test post.email then persona.email else post.email email: if /^sage$/.test post.email then persona.email else post.email
sub: if Conf['Remember Subject'] then post.sub else undefined sub: if Conf['Remember Subject'] then post.sub else undefined
flag: post.flag
$.set 'QR.persona', persona $.set 'QR.persona', persona
cooldown: cooldown:
init: -> init: ->
return unless Conf['Cooldown'] return unless Conf['Cooldown']
board = g.BOARD.ID setTimers = (e) => QR.cooldown.types = e.detail
QR.cooldown.types = $.on window, 'cooldown:timers', setTimers
thread: switch board $.globalEval 'window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))'
when 'q' then 86400 QR.cooldown.types or= {} # XXX tmp workaround until all pages and the catalogs get the cooldowns var.
when 'b', 'soc', 'r9k' then 600 $.off window, 'cooldown:timers', setTimers
else 300 for type of QR.cooldown.types
sage: if board is 'q' then 600 else 60 QR.cooldown.types[type] = +QR.cooldown.types[type]
file: if board is 'q' then 300 else 30
post: if board is 'q' then 150 else 30
QR.cooldown.upSpd = 0 QR.cooldown.upSpd = 0
QR.cooldown.upSpdAccuracy = .5 QR.cooldown.upSpdAccuracy = .5
$.get "cooldown.#{board}", {}, (item) -> key = "cooldown.#{g.BOARD}"
QR.cooldown.cooldowns = item["cooldown.#{board}"] $.get key, {}, (item) ->
QR.cooldown.cooldowns = item[key]
QR.cooldown.start() QR.cooldown.start()
$.sync "cooldown.#{board}", QR.cooldown.sync $.sync key, QR.cooldown.sync
start: -> start: ->
return unless Conf['Cooldown'] return unless Conf['Cooldown']
return if QR.cooldown.isCounting return if QR.cooldown.isCounting
@ -304,30 +303,16 @@ QR =
set: (data) -> set: (data) ->
return unless Conf['Cooldown'] return unless Conf['Cooldown']
{req, post, isReply, delay} = data {req, post, isReply, threadID, delay} = data
start = if req then req.uploadEndTime else Date.now() start = if req then req.uploadEndTime else Date.now()
if delay if delay
cooldown = {delay} cooldown = {delay}
else else
if post.file if hasFile = !!post.file
upSpd = post.file.size / ((req.uploadEndTime - req.uploadStartTime) / $.SECOND) upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND)
QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2 QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2
QR.cooldown.upSpd = upSpd QR.cooldown.upSpd = upSpd
isSage = /sage/i.test post.email cooldown = {isReply, hasFile, threadID}
hasFile = !!post.file
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 QR.cooldown.cooldowns[start] = cooldown
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns $.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
QR.cooldown.start() QR.cooldown.start()
@ -347,12 +332,12 @@ QR =
QR.status() QR.status()
return return
setTimeout QR.cooldown.count, $.SECOND clearTimeout QR.cooldown.timeout
QR.cooldown.timeout = setTimeout QR.cooldown.count, $.SECOND
now = Date.now() now = Date.now()
post = QR.posts[0] post = QR.posts[0]
isReply = post.thread isnt 'new' isReply = post.thread isnt 'new'
isSage = /sage/i.test post.email
hasFile = !!post.file hasFile = !!post.file
seconds = null seconds = null
{types, cooldowns, upSpd, upSpdAccuracy} = QR.cooldown {types, cooldowns, upSpd, upSpdAccuracy} = QR.cooldown
@ -366,26 +351,35 @@ QR =
QR.cooldown.unset start QR.cooldown.unset start
continue continue
if isReply is cooldown.isReply if 'timeout' of cooldown
# Only cooldowns relevant to this post can set the seconds value. # XXX tmp conversion from previous cooldowns
# 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) / $.SECOND
if elapsed >= 0 # clock changed since then?
seconds = Math.max seconds, types[type] - elapsed
if Conf['Cooldown Prediction'] and hasFile and upSpd
seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy
seconds = Math.max seconds, 0
unless start <= now <= cooldown.timeout
QR.cooldown.unset start QR.cooldown.unset start
continue
if isReply is cooldown.isReply
# Only cooldowns relevant to this post can set the seconds variable:
# reply cooldown with a reply, thread cooldown with a thread
elapsed = Math.floor (now - start) / $.SECOND
continue if elapsed < 0 # clock changed since then?
unless isReply
type = 'thread'
else if hasFile
# You can post an image reply immediately after a non-image reply.
unless cooldown.hasFile
seconds = Math.max seconds, 0
continue
type = 'image'
else
type = 'reply'
maxTimer = Math.max types[type] or 0, types[type + '_intra'] or 0
unless start <= now <= start + maxTimer * $.SECOND
QR.cooldown.unset start
type += '_intra' if isReply and +post.thread is cooldown.threadID
seconds = Math.max seconds, types[type] - elapsed
if seconds and Conf['Cooldown Prediction'] and hasFile and upSpd
seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy
seconds = if seconds > 0 then seconds else 0
# Update the status when we change posting type. # Update the status when we change posting type.
# Don't get stuck at some random number. # Don't get stuck at some random number.
# Don't interfere with progress status updates. # Don't interfere with progress status updates.
@ -575,6 +569,12 @@ QR =
if prev then prev.sub else persona.sub if prev then prev.sub else persona.sub
else else
'' ''
if QR.nodes.flag
@flag = if prev
prev.flag
else
persona.flag
@load() if QR.selected is @ # load persona @load() if QR.selected is @ # load persona
@select() if select @select() if select
@unlock() @unlock()
@ -596,8 +596,8 @@ QR =
lock: (lock=true) -> lock: (lock=true) ->
@isLocked = lock @isLocked = lock
return unless @ is QR.selected return unless @ is QR.selected
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler'] for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name]
QR.nodes[name].disabled = lock node.disabled = lock
@nodes.rm.style.visibility = if lock then 'hidden' else '' @nodes.rm.style.visibility = if lock then 'hidden' else ''
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput (if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
@nodes.spoiler.disabled = lock @nodes.spoiler.disabled = lock
@ -623,8 +623,10 @@ QR =
load: -> load: ->
# Load this post's values. # Load this post's values.
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename']
QR.nodes[name].value = @[name] or null for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
continue unless node = QR.nodes[name]
node.value = @[name] or node.dataset.default or null
QR.tripcodeHider.call QR.nodes['name'] QR.tripcodeHider.call QR.nodes['name']
@showFileData() @showFileData()
@ -635,7 +637,7 @@ QR =
@spoiler = input.checked @spoiler = input.checked
return return
{name} = input.dataset {name} = input.dataset
@[name] = input.value @[name] = input.value or input.dataset.default or null
switch name switch name
when 'thread' when 'thread'
QR.status() QR.status()
@ -660,8 +662,9 @@ QR =
return unless @ is QR.selected return unless @ is QR.selected
# Do this in case people use extensions # Do this in case people use extensions
# that do not trigger the `input` event. # that do not trigger the `input` event.
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler'] for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler', 'flag']
@save QR.nodes[name] continue unless node = QR.nodes[name]
@save node
return return
setFile: (@file) -> setFile: (@file) ->
@ -966,7 +969,13 @@ QR =
<option value=5>Loop</option> <option value=5>Loop</option>
<option value=4 selected>Other</option> <option value=4 selected>Other</option>
""" """
nodes.flashTag.dataset.default = '4'
$.add nodes.form, nodes.flashTag $.add nodes.form, nodes.flashTag
if flagSelector = $ '.flagSelector'
nodes.flag = flagSelector.cloneNode true
nodes.flag.dataset.name = 'flag'
nodes.flag.dataset.default = '0'
$.add nodes.form, nodes.flag
# Make a list of threads. # Make a list of threads.
for thread of g.BOARD.threads for thread of g.BOARD.threads
@ -1006,10 +1015,13 @@ QR =
$.on nodes[name], 'mouseover', QR.mouseover $.on nodes[name], 'mouseover', QR.mouseover
# save selected post's data # save selected post's data
items = ['name', 'email', 'sub', 'com', 'filename'] items = ['name', 'email', 'sub', 'com', 'filename', 'flag']
i = 0 i = 0
save = -> QR.selected.save @
while name = items[i++] while name = items[i++]
$.on nodes[name], 'input', -> QR.selected.save @ continue unless node = nodes[name]
event = if node.nodeName is 'SELECT' then 'change' else 'input'
$.on nodes[name], event, save
$.on nodes['name'], 'blur', QR.tripcodeHider $.on nodes['name'], 'blur', QR.tripcodeHider
$.on nodes.thread, 'change', -> QR.selected.save @ $.on nodes.thread, 'change', -> QR.selected.save @
@ -1068,7 +1080,7 @@ QR =
# prevent errors # prevent errors
if threadID is 'new' if threadID is 'new'
threadID = null threadID = null
if ['vg', 'q'].contains(g.BOARD.ID) and !post.sub if g.BOARD.ID is 'vg' and !post.sub
err = 'New threads require a subject.' err = 'New threads require a subject.'
else unless post.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm' else unless post.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
err = 'No file selected.' err = 'No file selected.'
@ -1104,7 +1116,7 @@ QR =
post.lock() post.lock()
postData = formData =
resto: threadID resto: threadID
name: post.name name: post.name
email: post.email email: post.email
@ -1113,6 +1125,7 @@ QR =
upfile: post.file upfile: post.file
filetag: filetag filetag: filetag
spoiler: post.spoiler spoiler: post.spoiler
flag: post.flag
textonly: textOnly textonly: textOnly
mode: 'regist' mode: 'regist'
pwd: QR.persona.pwd pwd: QR.persona.pwd
@ -1136,7 +1149,7 @@ QR =
[<a href="https://github.com/seaweedchan/4chan-x/wiki/Frequently-Asked-Questions#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean" target=_blank>?</a>] [<a href="https://github.com/seaweedchan/4chan-x/wiki/Frequently-Asked-Questions#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean" target=_blank>?</a>]
""" """
extra = extra =
form: $.formData postData form: $.formData formData
upCallbacks: upCallbacks:
onload: -> onload: ->
# Upload done, waiting for server response. # Upload done, waiting for server response.
@ -1241,9 +1254,9 @@ QR =
postID postID
} }
# Enable auto-posting if we have stuff to post, disable it otherwise. # Enable auto-posting if we have stuff left to post, disable it otherwise.
postsCount = QR.posts.length postsCount = QR.posts.length - 1
QR.cooldown.auto = postsCount > 1 and isReply QR.cooldown.auto = postsCount and isReply
if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount
notif = new Notification 'Quick reply warning', notif = new Notification 'Quick reply warning',
body: "You are running low on cached captchas. Cache count: #{captchasCount}." body: "You are running low on cached captchas. Cache count: #{captchasCount}."
@ -1262,7 +1275,7 @@ QR =
else else
post.rm() post.rm()
QR.cooldown.set {req, post, isReply} QR.cooldown.set {req, post, isReply, threadID}
URL = unless isReply # new thread URL = unless isReply # new thread
"/#{g.BOARD}/res/#{threadID}" "/#{g.BOARD}/res/#{threadID}"