From b90931b80d4509997d163b64f8b26c92beb34252 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Thu, 14 Feb 2013 21:31:31 +0100 Subject: [PATCH] unoop the thread updater. --- 4chan_x.user.js | 512 +++++++++++++++++++++----------------------- css/style.css | 10 +- src/features.coffee | 403 +++++++++++++++++----------------- src/main.coffee | 4 + src/qr.coffee | 4 +- 5 files changed, 452 insertions(+), 481 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index 7e82ba4ae..c7da62084 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -3786,16 +3786,62 @@ ThreadUpdater = { init: function() { + var checked, conf, html, name, _ref; if (g.VIEW !== 'thread' || !Conf['Thread Updater']) { return; } + html = ''; + _ref = Config.updater.checkbox; + for (name in _ref) { + conf = _ref[name]; + checked = Conf[name] ? 'checked' : ''; + html += "
"; + } + checked = Conf['Auto Update'] ? 'checked' : ''; + html = "
\n" + html + "\n
\n
\n
"; + this.dialog = UI.dialog('updater', 'bottom: 0; right: 0;', html); + this.timer = $('#update-timer', this.dialog); + this.status = $('#update-status', this.dialog); return Thread.prototype.callbacks.push({ name: 'Thread Updater', cb: this.node }); }, node: function() { - return new ThreadUpdater.Updater(this); + var input, _i, _len, _ref; + ThreadUpdater.thread = this; + ThreadUpdater.root = this.posts[this].nodes.root.parentNode; + ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]; + ThreadUpdater.outdateCount = 0; + ThreadUpdater.lastModified = '0'; + _ref = $$('input', ThreadUpdater.dialog); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + input = _ref[_i]; + if (input.type === 'checkbox') { + $.on(input, 'change', $.cb.checked); + } + switch (input.name) { + case 'Scroll BG': + $.on(input, 'change', ThreadUpdater.cb.scrollBG); + ThreadUpdater.cb.scrollBG(); + break; + case 'Auto Update This': + $.on(input, 'change', ThreadUpdater.cb.autoUpdate); + $.event('change', null, input); + break; + case 'Interval': + $.on(input, 'change', ThreadUpdater.cb.interval); + ThreadUpdater.cb.interval.call(input); + break; + case 'Update Now': + $.on(input, 'click', ThreadUpdater.update); + } + } + $.on(window, 'online offline', ThreadUpdater.cb.online); + $.on(d, 'QRPostSuccessful', ThreadUpdater.cb.post); + $.on(d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', ThreadUpdater.cb.visibility); + ThreadUpdater.cb.online(); + return $.add(d.body, ThreadUpdater.dialog); }, /* http://freesound.org/people/pierrecartoons1979/sounds/90112/ @@ -3803,279 +3849,204 @@ */ 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', - Updater: (function() { + cb: { + online: function() { + if (ThreadUpdater.online = navigator.onLine) { + ThreadUpdater.outdateCount = 0; + ThreadUpdater.set('timer', ThreadUpdater.getInterval()); + if (Conf['Auto Update This']) { + ThreadUpdater.update(); + } + ThreadUpdater.set('status', null, null); + } else { + ThreadUpdater.set('timer', null); + ThreadUpdater.set('status', 'Offline', 'warning'); + } + return ThreadUpdater.cb.autoUpdate(); + }, + post: function(e) { + if (!(Conf['Auto Update This'] && +e.detail.threadID === this.thread.ID)) { + return; + } + this.outdateCount = 0; + if (this.seconds > 2) { + return setTimeout(this.update.bind(this), 1000); + } + }, + visibility: function() { + if ($.hidden()) { + return; + } + ThreadUpdater.outdateCount = 0; + if (ThreadUpdater.seconds > ThreadUpdater.interval) { + return ThreadUpdater.set('timer', ThreadUpdater.getInterval()); + } + }, + scrollBG: function() { + return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? function() { + return true; + } : function() { + return !$.hidden(); + }; + }, + autoUpdate: function() { + if (Conf['Auto Update This'] && ThreadUpdater.online) { + return ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); + } else { + return clearTimeout(ThreadUpdater.timeoutID); + } + }, + interval: function() { + var val; + val = Math.max(5, parseInt(this.value, 10)); + ThreadUpdater.interval = this.value = val; + return $.cb.value.call(this); + }, + load: function() { + var klass, req, text, _ref, _ref1; + req = ThreadUpdater.req; + switch (req.status) { + case 200: + ThreadUpdater.parse(JSON.parse(req.response).posts); + ThreadUpdater.lastModified = req.getResponseHeader('Last-Modified'); + ThreadUpdater.set('timer', ThreadUpdater.getInterval()); + break; + case 404: + ThreadUpdater.set('timer', null); + ThreadUpdater.set('status', '404', 'warning'); + clearTimeout(ThreadUpdater.timeoutID); + ThreadUpdater.thread.kill(); + ThreadUpdater.outdateCount++; + ThreadUpdater.set('timer', ThreadUpdater.getInterval()); + break; + default: + ThreadUpdater.outdateCount++; + ThreadUpdater.set('timer', ThreadUpdater.getInterval()); + /* + Status Code 304: Not modified + By sending the `If-Modified-Since` header we get a proper status code, and no response. + This saves bandwidth for both the user and the servers and avoid unnecessary computation. + */ - function _Class(thread) { - var checked, dialog, html, input, name, title, val, _i, _len, _ref, _ref1; - this.thread = thread; - html = '
'; - _ref = Config.updater.checkbox; - for (name in _ref) { - val = _ref[name]; - title = val[1]; - checked = Conf[name] ? 'checked' : ''; - html += "
"; + _ref1 = (_ref = req.status) === 0 || _ref === 304 ? [null, null] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref1[0], klass = _ref1[1]; + ThreadUpdater.set('status', text, klass); } - checked = Conf['Auto Update'] ? 'checked' : ''; - html += "
\n
\n
"; - dialog = UI.dialog('updater', 'bottom: 0; right: 0;', html); - this.timer = $('#timer', dialog); - this.status = $('#status', dialog); - this.unsuccessfulFetchCount = 0; - this.lastModified = '0'; - this.threadRoot = thread.posts[thread].nodes.root.parentNode; - this.lastPost = +this.threadRoot.lastElementChild.id.slice(2); - _ref1 = $$('input', dialog); - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - input = _ref1[_i]; - if (input.type === 'checkbox') { - $.on(input, 'click', this.cb.checkbox.bind(this)); - $.event('click', null, input); - } - switch (input.name) { - case 'Scroll BG': - $.on(input, 'click', this.cb.scrollBG.bind(this)); - this.cb.scrollBG.call(this); - break; - case 'Auto Update This': - $.on(input, 'click', this.cb.autoUpdate.bind(this)); - break; - case 'Interval': - $.on(input, 'change', this.cb.interval.bind(this)); - $.event('change', null, input); - break; - case 'Update Now': - $.on(input, 'click', this.update.bind(this)); - } - } - $.on(window, 'online offline', this.cb.online.bind(this)); - $.on(d, 'QRPostSuccessful', this.cb.post.bind(this)); - $.on(d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', this.cb.visibility.bind(this)); - this.cb.online.call(this); - $.add(d.body, dialog); + return delete ThreadUpdater.req; } - - _Class.prototype.cb = { - online: function() { - if (this.online = navigator.onLine) { - this.unsuccessfulFetchCount = 0; - this.set('timer', this.getInterval()); - if (Conf['Auto Update This']) { - this.update(); - } - this.set('status', null); - this.status.className = null; - } else { - this.status.className = 'warning'; - this.set('status', 'Offline'); - this.set('timer', null); - } - return this.cb.autoUpdate.call(this); - }, - post: function(e) { - if (!(this['Auto Update This'] && +e.detail.threadID === this.thread.ID)) { - return; - } - this.unsuccessfulFetchCount = 0; - if (this.seconds > 2) { - return setTimeout(this.update.bind(this), 1000); - } - }, - visibility: function() { - if ($.hidden()) { - return; - } - this.unsuccessfulFetchCount = 0; - if (this.seconds > this.interval) { - return this.set('timer', this.getInterval()); - } - }, - checkbox: function(e) { - var checked, input, name; - input = e.target; - checked = input.checked, name = input.name; - this[name] = checked; - return $.cb.checked.call(input); - }, - scrollBG: function() { - return this.scrollBG = this['Scroll BG'] ? function() { - return true; - } : function() { - return !$.hidden(); - }; - }, - autoUpdate: function() { - if (this['Auto Update This'] && this.online) { - return this.timeoutID = setTimeout(this.timeout.bind(this), 1000); - } else { - return clearTimeout(this.timeoutID); - } - }, - interval: function(e) { - var input, val; - input = e.target; - val = Math.max(5, parseInt(input.value, 10)); - this.interval = input.value = val; - return $.cb.value.call(input); - }, - load: function() { - switch (this.req.status) { - case 404: - this.set('timer', null); - this.set('status', '404'); - this.status.className = 'warning'; - clearTimeout(this.timeoutID); - this.thread.isDead = true; - break; - case 0: - case 304: - /* - Status Code 304: Not modified - By sending the `If-Modified-Since` header we get a proper status code, and no response. - This saves bandwidth for both the user and the servers and avoid unnecessary computation. - */ - - this.unsuccessfulFetchCount++; - this.set('timer', this.getInterval()); - this.set('status', null); - this.status.className = null; - break; - case 200: - this.parse(JSON.parse(this.req.response).posts); - this.lastModified = this.req.getResponseHeader('Last-Modified'); - this.set('timer', this.getInterval()); - break; - default: - this.unsuccessfulFetchCount++; - this.set('timer', this.getInterval()); - this.set('status', "" + this.req.statusText + " (" + this.req.status + ")"); - this.status.className = 'warning'; - } - return delete this.req; + }, + getInterval: function() { + var i, j; + i = ThreadUpdater.interval; + j = Math.min(ThreadUpdater.outdateCount, 10); + if (!$.hidden()) { + j = Math.min(j, 7); + } + return ThreadUpdater.seconds = Math.max(i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]); + }, + set: function(name, text, klass) { + var el, node; + el = ThreadUpdater[name]; + if (node = el.firstChild) { + node.data = text; + } else { + el.textContent = text; + } + if (klass !== void 0) { + return el.className = klass; + } + }, + timeout: function() { + var n; + ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); + if (!(n = --ThreadUpdater.seconds)) { + return ThreadUpdater.update(); + } else if (n <= -60) { + ThreadUpdater.set('status', 'Retrying', null); + return ThreadUpdater.update(); + } else if (n > 0) { + return ThreadUpdater.set('timer', n); + } + }, + update: function() { + var url; + if (!ThreadUpdater.online) { + return; + } + ThreadUpdater.seconds = 0; + ThreadUpdater.set('timer', '...'); + if (ThreadUpdater.req) { + ThreadUpdater.req.onloadend = null; + ThreadUpdater.req.abort(); + } + url = "//api.4chan.org/" + ThreadUpdater.thread.board + "/res/" + ThreadUpdater.thread + ".json"; + return ThreadUpdater.req = $.ajax(url, { + onloadend: ThreadUpdater.cb.load + }, { + headers: { + 'If-Modified-Since': ThreadUpdater.lastModified } - }; - - _Class.prototype.getInterval = function() { - var i, j; - i = this.interval; - j = Math.min(this.unsuccessfulFetchCount, 10); - if (!$.hidden()) { - j = Math.min(j, 7); + }); + }, + parse: function(postObjects) { + var ID, count, files, index, node, nodes, num, post, postObject, posts, scroll, _i, _len, _ref; + Build.spoilerRange[ThreadUpdater.thread.board] = postObjects[0].custom_spoiler; + nodes = []; + posts = []; + index = []; + files = []; + count = 0; + for (_i = 0, _len = postObjects.length; _i < _len; _i++) { + postObject = postObjects[_i]; + num = postObject.no; + index.push(num); + if (postObject.fsize) { + files.push(num); } - return this.seconds = Math.max(i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]); - }; - - _Class.prototype.set = function(name, text) { - var el, node; - el = this[name]; - if (node = el.firstChild) { - return node.data = text; - } else { - return el.textContent = text; + if (num <= ThreadUpdater.lastPost) { + continue; } - }; - - _Class.prototype.timeout = function() { - var n; - this.timeoutID = setTimeout(this.timeout.bind(this), 1000); - if (!(n = --this.seconds)) { - return this.update(); - } else if (n <= -60) { - this.set('status', 'Retrying'); - this.status.className = null; - return this.update(); - } else if (n > 0) { - return this.set('timer', n); + count++; + node = Build.postFromObject(postObject, ThreadUpdater.thread.board.ID); + nodes.push(node); + posts.push(new Post(node, ThreadUpdater.thread, ThreadUpdater.thread.board)); + } + _ref = ThreadUpdater.thread.posts; + for (ID in _ref) { + post = _ref[ID]; + if (post.isDead) { + continue; } - }; - - _Class.prototype.update = function() { - var url; - if (!this.online) { - return; + ID = +ID; + if (-1 === index.indexOf(ID)) { + post.kill(); + } else if (post.file && !post.file.isDead && -1 === files.indexOf(ID)) { + post.kill(true); } - this.seconds = 0; - this.set('timer', '...'); - if (this.req) { - this.req.onloadend = null; - this.req.abort(); - } - url = "//api.4chan.org/" + this.thread.board + "/res/" + this.thread + ".json"; - return this.req = $.ajax(url, { - onloadend: this.cb.load.bind(this) - }, { - headers: { - 'If-Modified-Since': this.lastModified + } + if (count) { + if (Conf['Beep'] && $.hidden()) { + if (!ThreadUpdater.audio) { + ThreadUpdater.audio = $.el('audio', { + src: ThreadUpdater.beep + }); } - }); - }; - - _Class.prototype.parse = function(postObjects) { - var ID, count, i, image, index, node, nodes, num, post, postObject, posts, scroll, _i, _len, _ref; - Build.spoilerRange[this.thread.board] = postObjects[0].custom_spoiler; - nodes = []; - posts = []; - index = []; - image = []; - count = 0; - for (_i = 0, _len = postObjects.length; _i < _len; _i++) { - postObject = postObjects[_i]; - num = postObject.no; - index.push(num); - if (postObject.ext) { - image.push(num); - } - if (num <= this.lastPost) { - continue; - } - count++; - node = Build.postFromObject(postObject, this.thread.board.ID); - nodes.push(node); - posts.push(new Post(node, this.thread, this.thread.board)); + ThreadUpdater.audio.play(); } - _ref = this.thread.posts; - for (i in _ref) { - post = _ref[i]; - if (post.isDead) { - continue; - } - ID = post.ID; - if (-1 === index.indexOf(ID)) { - post.kill(); - } else if (post.file && !post.file.isDead && -1 === image.indexOf(ID)) { - post.kill(true); - } - } - if (count) { - if (Conf['Beep'] && $.hidden() && (Unread.replies.length === 0)) { - if (!this.audio) { - this.audio = $.el('audio', { - src: ThreadUpdater.beep - }); - } - audio.play(); - } - this.set('status', "+" + count); - this.status.className = 'new'; - this.unsuccessfulFetchCount = 0; - } else { - this.set('status', null); - this.status.className = null; - this.unsuccessfulFetchCount++; - return; - } - this.lastPost = posts[count - 1].ID; - Main.callbackNodes(Post, posts); - scroll = this['Auto Scroll'] && this.scrollBG() && this.threadRoot.getBoundingClientRect().bottom - doc.clientHeight < 25; - $.add(this.threadRoot, nodes); - if (scroll) { - return nodes[0].scrollIntoView(); - } - }; - - return _Class; - - })() + ThreadUpdater.set('status', "+" + count, 'new'); + ThreadUpdater.outdateCount = 0; + } else { + ThreadUpdater.set('status', null, null); + ThreadUpdater.outdateCount++; + return; + } + ThreadUpdater.lastPost = posts[count - 1].ID; + Main.callbackNodes(Post, posts); + scroll = Conf['Auto Scroll'] && ThreadUpdater.scrollBG() && ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25; + $.add(ThreadUpdater.root, nodes); + if (scroll) { + return nodes[0].scrollIntoView(); + } + } }; QR = { @@ -4824,7 +4795,7 @@ QR.cooldown.init(); QR.captcha.init(); $.add(d.body, QR.el); - return $.event(new CustomEvent('QRDialogCreation', null, QR.el)); + return $.event('QRDialogCreation', null, QR.el); }, submit: function(e) { var callbacks, captcha, captchas, challenge, err, filetag, m, opts, post, reply, response, textOnly, threadID, _ref; @@ -4987,10 +4958,10 @@ }; $.set('QR.persona', persona); _ref1 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref1[0], threadID = _ref1[1], postID = _ref1[2]; - $.event(new CustomEvent('QRPostSuccessful', { + $.event('QRPostSuccessful', { threadID: threadID, postID: postID - }, QR.el)); + }, QR.el); QR.cooldown.set({ post: reply, isReply: threadID !== '0' @@ -5052,6 +5023,11 @@ g.threads["" + board + "." + this] = board.threads[this] = this; } + Thread.prototype.kill = function() { + this.isDead = true; + return this.timeOfDeath = Date.now(); + }; + return Thread; })(); @@ -5579,7 +5555,7 @@ }); return [message, error]; }, - css: "/* General */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.field {\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\n-webkit-transition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field:-moz-placeholder,\n.field:hover:-moz-placeholder {\ncolor: #AAA !important;\n}\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\n-webkit-transition: all .1s ease-in-out;\ntransition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nbox-shadow: none;\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n-webkit-transition: all .75s .25s ease-in-out;\ntransition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* Notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\n-webkit-transition: all .25s ease-in-out;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Thread Updater */\n#updater {\ntext-align: right;\n}\n#updater:not(:hover) {\nbackground: none;\nborder: none;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink, .quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n:root.fit-width .full-image {\nmax-width: 100%;\n}\n:root.gecko.fit-width .full-image,\n:root.presto.fit-width .full-image {\nwidth: 100%;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* QR */\n.hide-original-post-form #postForm,\n.hide-original-post-form .postingMode {\ndisplay: none;\n}\n#qr > .move {\nmin-width: 300px;\noverflow: hidden;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 0 2px;\n}\n#qr > .move > span {\nfloat: right;\n}\n#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ncursor: pointer;\n}\n#qr select {\nmargin: 0;\n}\n#dump {\nbackground: -webkit-linear-gradient(#EEE, #CCC);\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\nwidth: 30px;\n}\n.gecko #dump {\npadding: 1px 0 2px;\nwidth: 10%;\n}\n#dump:hover, #dump:focus {\nbackground: -webkit-linear-gradient(#FFF, #DDD);\nbackground: linear-gradient(#FFF, #DDD);\n}\n#dump:active, .dump #dump:not(:hover):not(:focus) {\nbackground: -webkit-linear-gradient(#CCC, #DDD);\nbackground: linear-gradient(#CCC, #DDD);\n}\n#qr:not(.dump) #replies, .dump > form > label {\ndisplay: none;\n}\n#replies {\ndisplay: block;\nheight: 100px;\nposition: relative;\n-webkit-user-select: none;\n-moz-user-select: none;\n-o-user-select: none;\nuser-select: none;\n}\n#replies > div {\ncounter-reset: qrpreviews;\ntop: 0; right: 0; bottom: 0; left: 0;\nmargin: 0; padding: 0;\noverflow: hidden;\nposition: absolute;\nwhite-space: pre;\n}\n#replies > div:hover {\nbottom: -10px;\noverflow-x: auto;\nz-index: 1;\n}\n.qrpreview {\nbackground-position: 50% 20%;\nbackground-size: cover;\nborder: 1px solid #808080;\ncolor: #FFF !important;\nfont-size: 12px;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncursor: move;\ndisplay: inline-block;\nheight: 90px; width: 90px;\nmargin: 5px; padding: 2px;\nopacity: .6;\noutline: none;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-webkit-transition: opacity .25s ease-in-out;\ntransition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qrpreview:hover, .qrpreview:focus {\nopacity: .9;\ncolor: #FFF !important;\n}\n.qrpreview#selected {\nopacity: 1;\n}\n.qrpreview::before {\ncounter-increment: qrpreviews;\ncontent: counter(qrpreviews);\nfont-weight: 700;\ntext-shadow: 0 0 3px #000, 0 0 5px #000;\nposition: absolute;\ntop: 3px; right: 3px;\n}\n.qrpreview.drag {\nborder-color: red;\nborder-style: dashed;\n}\n.qrpreview.over {\nborder-color: #FFF;\nborder-style: dashed;\n}\n.remove {\ncolor: #E00 !important;\nfont-weight: 700;\npadding: 3px;\n}\n.remove:hover::after {\ncontent: ' Remove';\n}\n.qrpreview > label {\nbackground: rgba(0, 0, 0, .5);\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qrpreview > label > input {\nmargin: 1px 0;\nvertical-align: bottom;\n}\n#addReply {\nfont-size: 3.5em;\nline-height: 100px;\n}\n.persona {\ndisplay: -webkit-flex;\ndisplay: flex;\n}\n.persona .field {\n-webkit-flex: 1;\nflex: 1;\n}\n.gecko .persona .fieldĀ {\nwidth: 30%;\n}\n#qr textarea.field {\ndisplay: -webkit-box;\nmin-height: 160px;\nmin-width: 100%;\n}\n#qr.captcha textarea.field {\nmin-height: 120px;\n}\n.textarea {\nposition: relative;\n}\n#charCount {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nmargin: 1px;\nposition: absolute;\nbottom: 0;\nright: 0;\npointer-events: none;\n}\n#charCount.warning {\ncolor: red;\n}\n.captchainput > .field {\nmin-width: 100%;\n}\n.captchaimg {\nbackground: #FFF;\noutline: 1px solid #CCC;\noutline-offset: -1px;\ntext-align: center;\n}\n.captchaimg > img {\ndisplay: block;\nheight: 57px;\nwidth: 300px;\n}\n#qr [type=file] {\nmargin: 1px 0;\nwidth: 70%;\n}\n#qr [type=submit] {\nmargin: 1px 0;\npadding: 1px; /* not Gecko */\nwidth: 30%;\n}\n.gecko #qr [type=submit] {\npadding: 0 1px; /* Gecko does not respect box-sizing: border-box */\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: '';\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* General */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba-b .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.futaba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.burichan .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow .field:focus {\nborder-color: #000;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n:root.tomorrow .qrpreview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* General */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.photon .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" + css: "/* General */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.field {\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\n-webkit-transition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field:-moz-placeholder,\n.field:hover:-moz-placeholder {\ncolor: #AAA !important;\n}\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\n-webkit-transition: all .1s ease-in-out;\ntransition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nbox-shadow: none;\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n-webkit-transition: all .75s .25s ease-in-out;\ntransition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* Notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\n-webkit-transition: all .25s ease-in-out;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Thread Updater */\n#updater:not(:hover) {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n#updater > .move {\npadding: 0 3px;\n}\n#updater > div:last-child {\ntext-align: center;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink, .quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n:root.fit-width .full-image {\nmax-width: 100%;\n}\n:root.gecko.fit-width .full-image,\n:root.presto.fit-width .full-image {\nwidth: 100%;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* QR */\n.hide-original-post-form #postForm,\n.hide-original-post-form .postingMode {\ndisplay: none;\n}\n#qr > .move {\nmin-width: 300px;\noverflow: hidden;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 0 2px;\n}\n#qr > .move > span {\nfloat: right;\n}\n#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ncursor: pointer;\n}\n#qr select {\nmargin: 0;\n}\n#dump {\nbackground: -webkit-linear-gradient(#EEE, #CCC);\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\nwidth: 30px;\n}\n.gecko #dump {\npadding: 1px 0 2px;\nwidth: 10%;\n}\n#dump:hover, #dump:focus {\nbackground: -webkit-linear-gradient(#FFF, #DDD);\nbackground: linear-gradient(#FFF, #DDD);\n}\n#dump:active, .dump #dump:not(:hover):not(:focus) {\nbackground: -webkit-linear-gradient(#CCC, #DDD);\nbackground: linear-gradient(#CCC, #DDD);\n}\n#qr:not(.dump) #replies, .dump > form > label {\ndisplay: none;\n}\n#replies {\ndisplay: block;\nheight: 100px;\nposition: relative;\n-webkit-user-select: none;\n-moz-user-select: none;\n-o-user-select: none;\nuser-select: none;\n}\n#replies > div {\ncounter-reset: qrpreviews;\ntop: 0; right: 0; bottom: 0; left: 0;\nmargin: 0; padding: 0;\noverflow: hidden;\nposition: absolute;\nwhite-space: pre;\n}\n#replies > div:hover {\nbottom: -10px;\noverflow-x: auto;\nz-index: 1;\n}\n.qrpreview {\nbackground-position: 50% 20%;\nbackground-size: cover;\nborder: 1px solid #808080;\ncolor: #FFF !important;\nfont-size: 12px;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncursor: move;\ndisplay: inline-block;\nheight: 90px; width: 90px;\nmargin: 5px; padding: 2px;\nopacity: .6;\noutline: none;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-webkit-transition: opacity .25s ease-in-out;\ntransition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qrpreview:hover, .qrpreview:focus {\nopacity: .9;\ncolor: #FFF !important;\n}\n.qrpreview#selected {\nopacity: 1;\n}\n.qrpreview::before {\ncounter-increment: qrpreviews;\ncontent: counter(qrpreviews);\nfont-weight: 700;\ntext-shadow: 0 0 3px #000, 0 0 5px #000;\nposition: absolute;\ntop: 3px; right: 3px;\n}\n.qrpreview.drag {\nborder-color: red;\nborder-style: dashed;\n}\n.qrpreview.over {\nborder-color: #FFF;\nborder-style: dashed;\n}\n.remove {\ncolor: #E00 !important;\nfont-weight: 700;\npadding: 3px;\n}\n.remove:hover::after {\ncontent: ' Remove';\n}\n.qrpreview > label {\nbackground: rgba(0, 0, 0, .5);\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qrpreview > label > input {\nmargin: 1px 0;\nvertical-align: bottom;\n}\n#addReply {\nfont-size: 3.5em;\nline-height: 100px;\n}\n.persona {\ndisplay: -webkit-flex;\ndisplay: flex;\n}\n.persona .field {\n-webkit-flex: 1;\nflex: 1;\n}\n.gecko .persona .fieldĀ {\nwidth: 30%;\n}\n#qr textarea.field {\ndisplay: -webkit-box;\nmin-height: 160px;\nmin-width: 100%;\n}\n#qr.captcha textarea.field {\nmin-height: 120px;\n}\n.textarea {\nposition: relative;\n}\n#charCount {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nmargin: 1px;\nposition: absolute;\nbottom: 0;\nright: 0;\npointer-events: none;\n}\n#charCount.warning {\ncolor: red;\n}\n.captchainput > .field {\nmin-width: 100%;\n}\n.captchaimg {\nbackground: #FFF;\noutline: 1px solid #CCC;\noutline-offset: -1px;\ntext-align: center;\n}\n.captchaimg > img {\ndisplay: block;\nheight: 57px;\nwidth: 300px;\n}\n#qr [type=file] {\nmargin: 1px 0;\nwidth: 70%;\n}\n#qr [type=submit] {\nmargin: 1px 0;\npadding: 1px; /* not Gecko */\nwidth: 30%;\n}\n.gecko #qr [type=submit] {\npadding: 0 1px; /* Gecko does not respect box-sizing: border-box */\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: '';\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* General */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba-b .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.futaba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.burichan .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* General */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow .field:focus {\nborder-color: #000;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n:root.tomorrow .qrpreview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* General */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.photon .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" }; Main.init(); diff --git a/css/style.css b/css/style.css index 2729ea628..55b1b420f 100644 --- a/css/style.css +++ b/css/style.css @@ -178,12 +178,16 @@ a[href="javascript:;"] { } /* Thread Updater */ -#updater { - text-align: right; -} #updater:not(:hover) { background: none; border: none; + box-shadow: none; +} +#updater > .move { + padding: 0 3px; +} +#updater > div:last-child { + text-align: center; } #updater input[type=number] { width: 4em; diff --git a/src/features.coffee b/src/features.coffee index 25c646038..35fe715ea 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -2313,234 +2313,221 @@ ThreadUpdater = init: -> return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] + html = '' + for name, conf of Config.updater.checkbox + checked = if Conf[name] then 'checked' else '' + html += "
" + + checked = if Conf['Auto Update'] then 'checked' else '' + html = """ +
+ #{html} +
+
+
+ """ + + @dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html + @timer = $ '#update-timer', @dialog + @status = $ '#update-status', @dialog + Thread::callbacks.push name: 'Thread Updater' cb: @node + node: -> - new ThreadUpdater.Updater @ + ThreadUpdater.thread = @ + ThreadUpdater.root = @posts[@].nodes.root.parentNode + ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0] + ThreadUpdater.outdateCount = 0 + ThreadUpdater.lastModified = '0' + + for input in $$ 'input', ThreadUpdater.dialog + if input.type is 'checkbox' + $.on input, 'change', $.cb.checked + switch input.name + when 'Scroll BG' + $.on input, 'change', ThreadUpdater.cb.scrollBG + ThreadUpdater.cb.scrollBG() + when 'Auto Update This' + $.on input, 'change', ThreadUpdater.cb.autoUpdate + $.event 'change', null, input + when 'Interval' + $.on input, 'change', ThreadUpdater.cb.interval + ThreadUpdater.cb.interval.call input + when 'Update Now' + $.on input, 'click', ThreadUpdater.update + + $.on window, 'online offline', ThreadUpdater.cb.online + $.on d, 'QRPostSuccessful', ThreadUpdater.cb.post + $.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', ThreadUpdater.cb.visibility + + ThreadUpdater.cb.online() + $.add d.body, ThreadUpdater.dialog + ### http://freesound.org/people/pierrecartoons1979/sounds/90112/ cc-by-nc-3.0 ### 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' - - Updater: class - constructor: (@thread) -> - html = '
' - for name, val of Config.updater.checkbox - title = val[1] - checked = if Conf[name] then 'checked' else '' - html += "
" - - checked = if Conf['Auto Update'] then 'checked' else '' - html += """ -
-
-
- """ - - dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html - - @timer = $ '#timer', dialog - @status = $ '#status', dialog - - @unsuccessfulFetchCount = 0 - @lastModified = '0' - @threadRoot = thread.posts[thread].nodes.root.parentNode - @lastPost = +@threadRoot.lastElementChild.id[2..] - - for input in $$ 'input', dialog - if input.type is 'checkbox' - $.on input, 'click', @cb.checkbox.bind @ - $.event 'click', null, input - switch input.name - when 'Scroll BG' - $.on input, 'click', @cb.scrollBG.bind @ - @cb.scrollBG.call @ - when 'Auto Update This' - $.on input, 'click', @cb.autoUpdate.bind @ - when 'Interval' - $.on input, 'change', @cb.interval.bind @ - $.event 'change', null, input - when 'Update Now' - $.on input, 'click', @update.bind @ - - $.on window, 'online offline', @cb.online.bind @ - $.on d, 'QRPostSuccessful', @cb.post.bind @ - $.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', @cb.visibility.bind @ - - @cb.online.call @ - $.add d.body, dialog - - cb: - online: -> - if @online = navigator.onLine - @unsuccessfulFetchCount = 0 - @set 'timer', @getInterval() - @update() if Conf['Auto Update This'] - @set 'status', null - @status.className = null + cb: + online: -> + if ThreadUpdater.online = navigator.onLine + ThreadUpdater.outdateCount = 0 + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + ThreadUpdater.update() if Conf['Auto Update This'] + ThreadUpdater.set 'status', null, null + else + ThreadUpdater.set 'timer', null + ThreadUpdater.set 'status', 'Offline', 'warning' + ThreadUpdater.cb.autoUpdate() + post: (e) -> + return unless Conf['Auto Update This'] and +e.detail.threadID is @thread.ID + @outdateCount = 0 + setTimeout @update.bind(@), 1000 if @seconds > 2 + visibility: -> + return if $.hidden() + # Reset the counter when we focus this tab. + ThreadUpdater.outdateCount = 0 + if ThreadUpdater.seconds > ThreadUpdater.interval + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + scrollBG: -> + ThreadUpdater.scrollBG = if Conf['Scroll BG'] + -> true + else + -> not $.hidden() + autoUpdate: -> + if Conf['Auto Update This'] and ThreadUpdater.online + ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 + else + clearTimeout ThreadUpdater.timeoutID + interval: -> + val = Math.max 5, parseInt @value, 10 + ThreadUpdater.interval = @value = val + $.cb.value.call @ + load: -> + {req} = ThreadUpdater + switch req.status + when 200 + ThreadUpdater.parse JSON.parse(req.response).posts + ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified' + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + when 404 + ThreadUpdater.set 'timer', null + ThreadUpdater.set 'status', '404', 'warning' + clearTimeout ThreadUpdater.timeoutID + ThreadUpdater.thread.kill() + # if Conf['Unread Count'] + # Unread.title = Unread.title.match(/^.+-/)[0] + ' 404' + # else + # d.title = d.title.match(/^.+-/)[0] + ' 404' + # Unread.update true + # QR.abort() + ThreadUpdater.outdateCount++ + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() else - @status.className = 'warning' - @set 'status', 'Offline' - @set 'timer', null - @cb.autoUpdate.call @ - post: (e) -> - return unless @['Auto Update This'] and +e.detail.threadID is @thread.ID - @unsuccessfulFetchCount = 0 - setTimeout @update.bind(@), 1000 if @seconds > 2 - visibility: -> - return if $.hidden() - # Reset the counter when we focus this tab. - @unsuccessfulFetchCount = 0 - if @seconds > @interval - @set 'timer', @getInterval() - checkbox: (e) -> - input = e.target - {checked, name} = input - @[name] = checked - $.cb.checked.call input - scrollBG: -> - @scrollBG = - if @['Scroll BG'] - -> true - else - -> not $.hidden() - autoUpdate: -> - if @['Auto Update This'] and @online - @timeoutID = setTimeout @timeout.bind(@), 1000 - else - clearTimeout @timeoutID - interval: (e) -> - input = e.target - val = Math.max 5, parseInt input.value, 10 - @interval = input.value = val - $.cb.value.call input - load: -> - switch @req.status - when 404 - @set 'timer', null - @set 'status', '404' - @status.className = 'warning' - clearTimeout @timeoutID - @thread.isDead = true - # if Conf['Unread Count'] - # Unread.title = Unread.title.match(/^.+-/)[0] + ' 404' - # else - # d.title = d.title.match(/^.+-/)[0] + ' 404' - # Unread.update true - # QR.abort() + ThreadUpdater.outdateCount++ + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + ### + Status Code 304: Not modified + By sending the `If-Modified-Since` header we get a proper status code, and no response. + This saves bandwidth for both the user and the servers and avoid unnecessary computation. + ### # XXX 304 -> 0 in Opera - when 0, 304 - ### - Status Code 304: Not modified - By sending the `If-Modified-Since` header we get a proper status code, and no response. - This saves bandwidth for both the user and the servers and avoid unnecessary computation. - ### - @unsuccessfulFetchCount++ - @set 'timer', @getInterval() - @set 'status', null - @status.className = null - when 200 - @parse JSON.parse(@req.response).posts - @lastModified = @req.getResponseHeader 'Last-Modified' - @set 'timer', @getInterval() + [text, klass] = if req.status in [0, 304] + [null, null] else - @unsuccessfulFetchCount++ - @set 'timer', @getInterval() - @set 'status', "#{@req.statusText} (#{@req.status})" - @status.className = 'warning' - delete @req + ["#{req.statusText} (#{req.status})", 'warning'] + ThreadUpdater.set 'status', text, klass + delete ThreadUpdater.req - getInterval: -> - i = @interval - j = Math.min @unsuccessfulFetchCount, 10 - unless $.hidden() - # Lower the max refresh rate limit on visible tabs. - j = Math.min j, 7 - @seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] + getInterval: -> + i = ThreadUpdater.interval + j = Math.min ThreadUpdater.outdateCount, 10 + unless $.hidden() + # Lower the max refresh rate limit on visible tabs. + j = Math.min j, 7 + ThreadUpdater.seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] - set: (name, text) -> - el = @[name] - if node = el.firstChild - # Prevent the creation of a new DOM Node - # by setting the text node's data. - node.data = text - else - el.textContent = text + set: (name, text, klass) -> + el = ThreadUpdater[name] + if node = el.firstChild + # Prevent the creation of a new DOM Node + # by setting the text node's data. + node.data = text + else + el.textContent = text + el.className = klass if klass isnt undefined - timeout: -> - @timeoutID = setTimeout @timeout.bind(@), 1000 - unless n = --@seconds - @update() - else if n <= -60 - @set 'status', 'Retrying' - @status.className = null - @update() - else if n > 0 - @set 'timer', n + timeout: -> + ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 + unless n = --ThreadUpdater.seconds + ThreadUpdater.update() + else if n <= -60 + ThreadUpdater.set 'status', 'Retrying', null + ThreadUpdater.update() + else if n > 0 + ThreadUpdater.set 'timer', n - update: -> - return unless @online - @seconds = 0 - @set 'timer', '...' - if @req - # abort() triggers onloadend, we don't want that. - @req.onloadend = null - @req.abort() - url = "//api.4chan.org/#{@thread.board}/res/#{@thread}.json" - @req = $.ajax url, onloadend: @cb.load.bind @, - headers: 'If-Modified-Since': @lastModified + update: -> + return unless ThreadUpdater.online + ThreadUpdater.seconds = 0 + ThreadUpdater.set 'timer', '...' + if ThreadUpdater.req + # abort() triggers onloadend, we don't want that. + ThreadUpdater.req.onloadend = null + ThreadUpdater.req.abort() + url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json" + ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load, + headers: 'If-Modified-Since': ThreadUpdater.lastModified - parse: (postObjects) -> - Build.spoilerRange[@thread.board] = postObjects[0].custom_spoiler + parse: (postObjects) -> + Build.spoilerRange[ThreadUpdater.thread.board] = postObjects[0].custom_spoiler - nodes = [] # post container elements - posts = [] # post objects - index = [] # existing posts - image = [] # existing images - count = 0 # new posts count - # Build the index, create posts. - for postObject in postObjects - num = postObject.no - index.push num - image.push num if postObject.ext - continue if num <= @lastPost - # Insert new posts, not older ones. - count++ - node = Build.postFromObject postObject, @thread.board.ID - nodes.push node - posts.push new Post node, @thread, @thread.board + nodes = [] # post container elements + posts = [] # post objects + index = [] # existing posts + files = [] # existing files + count = 0 # new posts count + # Build the index, create posts. + for postObject in postObjects + num = postObject.no + index.push num + files.push num if postObject.fsize + continue if num <= ThreadUpdater.lastPost + # Insert new posts, not older ones. + count++ + node = Build.postFromObject postObject, ThreadUpdater.thread.board.ID + nodes.push node + posts.push new Post node, ThreadUpdater.thread, ThreadUpdater.thread.board - # Check for deleted posts and deleted images. - for i, post of @thread.posts - continue if post.isDead - {ID} = post - if -1 is index.indexOf ID - post.kill() - else if post.file and !post.file.isDead and -1 is image.indexOf ID - post.kill true + # Check for deleted posts/files. + for ID, post of ThreadUpdater.thread.posts + continue if post.isDead + ID = +ID + if -1 is index.indexOf ID + post.kill() + else if post.file and !post.file.isDead and -1 is files.indexOf ID + post.kill true - if count - if Conf['Beep'] and $.hidden() and (Unread.replies.length is 0) - unless @audio - @audio = $.el 'audio', src: ThreadUpdater.beep - audio.play() - @set 'status', "+#{count}" - @status.className = 'new' - @unsuccessfulFetchCount = 0 - else - @set 'status', null - @status.className = null - @unsuccessfulFetchCount++ - return + if count + if Conf['Beep'] and $.hidden() #and !Unread.replies.length + unless ThreadUpdater.audio + ThreadUpdater.audio = $.el 'audio', src: ThreadUpdater.beep + ThreadUpdater.audio.play() + ThreadUpdater.set 'status', "+#{count}", 'new' + ThreadUpdater.outdateCount = 0 + else + ThreadUpdater.set 'status', null, null + ThreadUpdater.outdateCount++ + return - @lastPost = posts[count - 1].ID - Main.callbackNodes Post, posts + ThreadUpdater.lastPost = posts[count - 1].ID + Main.callbackNodes Post, posts - scroll = @['Auto Scroll'] and @scrollBG() and - @threadRoot.getBoundingClientRect().bottom - doc.clientHeight < 25 - $.add @threadRoot, nodes - if scroll - nodes[0].scrollIntoView() + scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and + ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25 + $.add ThreadUpdater.root, nodes + if scroll + nodes[0].scrollIntoView() diff --git a/src/main.coffee b/src/main.coffee index d0cdebe92..154b9a054 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -24,6 +24,10 @@ class Thread g.threads["#{board}.#{@}"] = board.threads[@] = @ + kill: -> + @isDead = true + @timeOfDeath = Date.now() + class Post callbacks: [] toString: -> @ID diff --git a/src/qr.coffee b/src/qr.coffee index a144d6ca5..56ead95f5 100644 --- a/src/qr.coffee +++ b/src/qr.coffee @@ -611,7 +611,7 @@ QR = # Create a custom event when the QR dialog is first initialized. # Use it to extend the QR's functionalities, or for XTRM RICE. - $.event new CustomEvent 'QRDialogCreation', null, QR.el + $.event 'QRDialogCreation', null, QR.el submit: (e) -> e?.preventDefault() @@ -777,7 +777,7 @@ QR = [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/ # Post/upload confirmed as successful. - $.event new CustomEvent 'QRPostSuccessful', { + $.event 'QRPostSuccessful', { threadID postID }, QR.el