diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index 32b0276bc..00d6134de 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -3141,26 +3141,24 @@ }; Callbacks.prototype.execute = function(nodes) { - var cb, err, errors, i, j, name, node; - i = 0; - while (name = this.keys[i++]) { - j = 0; - cb = this[name]; - while (node = nodes[j++]) { - try { - if (!cb.disconnected) { - cb.call(node); - } - } catch (_error) { - err = _error; - if (!errors) { - errors = []; - } - errors.push({ - message: ['"', name, '" crashed on node ', this.type, ' No.', node.ID, ' (', node.board, ').'].join(''), - error: err - }); + var err, errors, name, node, _i, _j, _len, _len1, _ref; + _ref = this.keys; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + name = _ref[_i]; + try { + for (_j = 0, _len1 = nodes.length; _j < _len1; _j++) { + node = nodes[_j]; + this[name].call(node); } + } catch (_error) { + err = _error; + if (!errors) { + errors = []; + } + errors.push({ + message: ['"', name, '" crashed on node ', this.type, ' No.', node.ID, ' (', node.board, ').'].join(''), + error: err + }); } } if (errors) { @@ -3216,13 +3214,16 @@ } Thread.prototype.setPage = function(pageNum) { - var icon, key, _i, _len, _ref; - icon = $('.page-num', this.OP.nodes.info); - _ref = ['title', 'textContent']; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - key = _ref[_i]; - icon[key] = icon[key].replace(/\d+/, pageNum); + var icon, info, quote, _ref; + _ref = this.OP.nodes, info = _ref.info, quote = _ref.quote; + if (!(icon = $('.page-num', info))) { + icon = $.el('span', { + className: 'page-num' + }); + $.after(quote, [$.tn(' '), icon]); } + icon.title = "This thread is on page " + pageNum + " in the original index."; + icon.textContent = "[" + pageNum + "]"; if (this.catalogView) { return this.catalogView.nodes.pageCount.textContent = pageNum; } @@ -3239,7 +3240,7 @@ }; Thread.prototype.setStatus = function(type, status) { - var name, typeLC; + var name; name = "is" + type; if (this[name] === status) { return; @@ -3248,7 +3249,6 @@ if (!this.OP) { return; } - typeLC = type.toLowerCase(); this.setIcon('Sticky', this.isSticky); this.setIcon('Closed', this.isClosed && !this.isArchived); return this.setIcon('Archived', this.isArchived); @@ -3275,7 +3275,7 @@ title: type, className: "" + typeLC + "Icon retina" }); - root = type !== 'Sticky' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : $('.page-num', this.OP.nodes.info) || $('[title="Reply to this post"]', this.OP.nodes.info); + root = type !== 'Sticky' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : $('.page-num', this.OP.nodes.info) || this.OP.nodes.quote; $.after(root, [$.tn(' '), icon]); if (!this.catalogView) { return; @@ -3349,11 +3349,12 @@ this.board = this.thread.board; this.nodes = { root: root, - thumb: $('.thumb', root), - icons: $('.thread-icons', root), + thumb: $('.catalog-thumb', root), + icons: $('.catalog-icons', root), postCount: $('.post-count', root), fileCount: $('.file-count', root), - pageCount: $('.page-count', root) + pageCount: $('.page-count', root), + comment: $('.comment', root) }; this.thread.catalogView = this; } @@ -3378,17 +3379,18 @@ } this.ID = +root.id.slice(2); this.fullID = "" + this.board + "." + this.ID; - post = $('.post', root); - info = $('.postInfo', post); if (that.isOriginalMarkup) { this.cleanup(root, post); } + post = $('.post', root); + info = $('.postInfo', post); root.dataset.fullID = this.fullID; this.nodes = { root: root, post: post, info: info, nameBlock: $('.nameBlock', info), + quote: $('.postNum > a:nth-of-type(2)', info), comment: $('.postMessage', post), links: [], quotelinks: [], @@ -3396,8 +3398,12 @@ }; if (!(this.isReply = $.hasClass(post, 'reply'))) { this.thread.OP = this; + this.thread.isArchived = !!$('.archivedIcon', info); this.thread.isSticky = !!$('.stickyIcon', info); - this.thread.isClosed = !!$('.closedIcon', info); + this.thread.isClosed = this.thread.isArchived || !!$('.closedIcon', info); + if (this.thread.isArchived) { + this.thread.kill(); + } } this.info = {}; this.info.nameBlock = Conf['Anonymize'] ? 'Anonymous' : this.nodes.nameBlock.textContent.trim(); @@ -3448,27 +3454,45 @@ } Post.prototype.parseComment = function() { - var bq, i, node, nodes, text; + var bq, i, node, nodes, spoilers; this.nodes.comment.normalize(); bq = this.nodes.comment.cloneNode(true); - nodes = $$('.abbr, .exif, b', bq); + nodes = $$('.abbr, .exif, b, marquee', bq); i = 0; while (node = nodes[i++]) { $.rm(node); } + this.info.comment = this.nodesToText(bq); + spoilers = $$('s', bq); + return this.info.commentSpoilered = (function() { + var _i, _len; + if (spoilers.length) { + for (_i = 0, _len = spoilers.length; _i < _len; _i++) { + node = spoilers[_i]; + $.replace(node, $.tn('[spoiler]')); + } + return this.nodesToText(bq); + } else { + return this.info.comment; + } + }).call(this); + }; + + Post.prototype.nodesToText = function(bq) { + var i, node, nodes, text; text = ""; nodes = $.X('.//br|.//text()', bq); i = 0; while (node = nodes.snapshotItem(i++)) { text += node.data || '\n'; } - return this.info.comment = text.trim().replace(/\s+$/gm, ''); + return text.trim().replace(/\s+$/gm, ''); }; Post.prototype.parseQuotes = function() { var quotelink, _i, _len, _ref; this.quotes = []; - _ref = $$('.quotelink', this.nodes.comment); + _ref = $$(':not(pre) > .quotelink', this.nodes.comment); for (_i = 0, _len = _ref.length; _i < _len; _i++) { quotelink = _ref[_i]; this.parseQuote(quotelink); @@ -3477,7 +3501,7 @@ Post.prototype.parseQuote = function(quotelink) { var fullID, match; - if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/thread\/\d+.*\#p(\d+)$/))) { + if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/(?:res|thread)\/\d+(?:\/[^#]*)?#p(\d+)$/))) { return; } this.nodes.quotelinks.push(quotelink); @@ -3511,30 +3535,26 @@ size *= 1024; } this.file.sizeInBytes = size; - this.file.thumbURL = that.isArchived ? thumb.src : "" + location.protocol + "//t.4cdn.org/" + this.board + "/" + (this.file.URL.match(/(\d+)\./)[1]) + "s.jpg"; + this.file.thumbURL = "" + location.protocol + "//t.4cdn.org/" + this.board + "/" + (this.file.URL.match(/(\d+)\./)[1]) + "s.jpg"; this.file.isImage = /(jpg|png|gif)$/i.test(this.file.URL); this.file.isVideo = /webm$/i.test(this.file.URL); + nameNode = $('a', fileText); if (this.file.isImage || this.file.isVideo) { - this.file.dimensions = fileText.childNodes[2].data.match(/\d+x\d+/)[0]; + this.file.dimensions = nameNode.nextSibling.textContent.match(/\d+x\d+/)[0]; } - return this.file.name = !this.file.isSpoiler && (nameNode = $('a', fileText)) ? nameNode.title || nameNode.textContent : fileText.title; + return this.file.name = fileText.title || nameNode.title || nameNode.textContent; }; - Post.prototype.cleanup = function(root, post) { - var node, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2; + Post.prototype.cleanup = function(root) { + var node, _i, _j, _len, _len1, _ref, _ref1; _ref = $$('.mobile', root); for (_i = 0, _len = _ref.length; _i < _len; _i++) { node = _ref[_i]; $.rm(node); } - _ref1 = $$('[id]:not(.exif)', post); + _ref1 = $$('.desktop', root); for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { node = _ref1[_j]; - node.removeAttribute('id'); - } - _ref2 = $$('.desktop', root); - for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { - node = _ref2[_k]; $.rmClass(node, 'desktop'); } }; @@ -3606,9 +3626,6 @@ return; } this.isHidden = false; - this.labels = this.labels.filter(function(label) { - return !/^(Manually hidden|Recursively hidden|Hidden by)/.test(label); - }); _ref = Get.allQuotelinksLinkingTo(this); for (_i = 0, _len = _ref.length; _i < _len; _i++) { quotelink = _ref[_i]; @@ -3637,8 +3654,7 @@ return delete this.nodes.stub; }; - Post.prototype.highlight = function(label, highlight, top) { - this.labels.push(label); + Post.prototype.highlight = function(highlight, top) { if (__indexOf.call(this.highlights, highlight) < 0) { this.highlights.push(highlight); $.addClass(this.nodes.root, highlight); @@ -3751,7 +3767,7 @@ __extends(Clone, _super); function Clone(origin, context, contractThumb) { - var file, info, inline, inlined, key, nodes, post, root, val, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + var file, info, inline, inlined, key, nodes, post, root, val, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3; this.origin = origin; this.context = context; _ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']; @@ -3760,13 +3776,15 @@ this[key] = origin[key]; } nodes = origin.nodes; - root = nodes.root.cloneNode(true); + root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true); post = $('.post', root); info = $('.postInfo', post); this.nodes = { root: root, post: post, info: info, + nameBlock: $('.nameBlock', info), + quote: $('.postNum > a:nth-of-type(2)', info), comment: $('.postMessage', post), quotelinks: [], backlinks: info.getElementsByClassName('backlink') @@ -3800,10 +3818,10 @@ this.nodes.uniqueID = $('.posteruid', info); } if (nodes.capcode) { - this.nodes.capcode = $('.capcode', info); + this.nodes.capcode = $('.capcode.hand', info); } if (nodes.flag) { - this.nodes.flag = $('.countryFlag', info); + this.nodes.flag = $('.flag, .countryFlag', info); } if (nodes.date) { this.nodes.date = $('.dateTime', info); @@ -3818,18 +3836,11 @@ } file = $('.file', post); this.file.text = file.firstElementChild; - this.file.thumb = $('img[data-md5], video[data-md5]', file); + this.file.thumb = $('.fileThumb > [data-md5]', file); this.file.fullImage = $('.full-image', file); + this.file.videoControls = $('.video-controls', this.file.text); if (contractThumb) { - $.rmClass(root, 'expanded-image'); - $.rmClass(this.file.thumb, 'expanding'); - } - this.file.isExpanded = $.hasClass(root, 'expanded-image'); - if ((_ref4 = this.file.fullImage) != null) { - _ref4.removeAttribute('id'); - } - if ((_ref5 = $('.video-controls', this.file.text)) != null) { - _ref5.remove(); + ImageExpand.contract(this); } } if (origin.isDead) { @@ -3839,6 +3850,23 @@ root.dataset.clone = origin.clones.push(this) - 1; } + Clone.prototype.cloneWithoutVideo = function(node) { + var child, clone, _i, _len, _ref; + if (node.tagName === 'VIDEO' && !node.dataset.md5) { + return []; + } else if (node.nodeType === Node.ELEMENT_NODE && $('video', node)) { + clone = node.cloneNode(false); + _ref = node.childNodes; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + $.add(clone, this.cloneWithoutVideo(child)); + } + return clone; + } else { + return node.cloneNode(true); + } + }; + return Clone; })(Post); @@ -4012,12 +4040,13 @@ })(); Notice = (function() { - function Notice(type, content, timeout) { + function Notice(type, content, timeout, onclose) { this.timeout = timeout; + this.onclose = onclose; this.close = __bind(this.close, this); this.add = __bind(this.add, this); this.el = $.el('div', { - innerHTML: '\uf00d
' + innerHTML: "
" }); this.el.style.opacity = 0; this.setType(type); @@ -4049,7 +4078,8 @@ Notice.prototype.close = function() { $.off(d, 'visibilitychange', this.add); - return $.rm(this.el); + $.rm(this.el); + return typeof this.onclose === "function" ? this.onclose() : void 0; }; return Notice; @@ -4089,7 +4119,7 @@ RandomAccessList.prototype.before = function(root, item) { var prev; - if (item.next === root) { + if (item.next === root || item === root) { return; } this.rmi(item); @@ -4099,6 +4129,8 @@ item.prev = prev; if (prev) { return prev.next = item; + } else { + return this.first = item; } }; @@ -4114,6 +4146,8 @@ item.next = next; if (next) { return next.prev = item; + } else { + return this.last = item; } }; @@ -4125,7 +4159,11 @@ } this.rmi(item); item.next = first; - first.prev = item; + if (first) { + first.prev = item; + } else { + this.last = item; + } this.first = item; return delete item.prev; }; @@ -4279,7 +4317,28 @@ })(); Polyfill = { - init: function() {}, + init: function() { + this.notificationPermission(); + this.toBlob(); + return this.visibility(); + }, + notificationPermission: function() { + if (!window.Notification || 'permission' in Notification || !window.webkitNotifications) { + return; + } + return Object.defineProperty(Notification, 'permission', { + get: function() { + switch (webkitNotifications.checkPermission()) { + case 0: + return 'granted'; + case 1: + return 'default'; + case 2: + return 'denied'; + } + } + }); + }, toBlob: function() { var _base; return (_base = HTMLCanvasElement.prototype).toBlob || (_base.toBlob = function(cb) { @@ -4294,6 +4353,26 @@ type: 'image/png' })); }); + }, + visibility: function() { + if ('visibilityState' in d) { + return; + } + Object.defineProperties(HTMLDocument.prototype, { + visibilityState: { + get: function() { + return this.webkitVisibilityState; + } + }, + hidden: { + get: function() { + return this.webkitHidden; + } + } + }); + return $.on(d, 'webkitvisibilitychange', function() { + return $.event('visibilitychange'); + }); } }; @@ -13442,7 +13521,7 @@ MarkNewIPs.ipCount = this.ipCount; MarkNewIPs.postIDs = (function() { var _i, _len, _ref, _results; - _ref = this.post.keys; + _ref = this.posts.keys; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { x = _ref[_i]; diff --git a/builds/crx/script.js b/builds/crx/script.js index 03acb5785..676bf4101 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -3167,26 +3167,24 @@ }; Callbacks.prototype.execute = function(nodes) { - var cb, err, errors, i, j, name, node; - i = 0; - while (name = this.keys[i++]) { - j = 0; - cb = this[name]; - while (node = nodes[j++]) { - try { - if (!cb.disconnected) { - cb.call(node); - } - } catch (_error) { - err = _error; - if (!errors) { - errors = []; - } - errors.push({ - message: ['"', name, '" crashed on node ', this.type, ' No.', node.ID, ' (', node.board, ').'].join(''), - error: err - }); + var err, errors, name, node, _i, _j, _len, _len1, _ref; + _ref = this.keys; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + name = _ref[_i]; + try { + for (_j = 0, _len1 = nodes.length; _j < _len1; _j++) { + node = nodes[_j]; + this[name].call(node); } + } catch (_error) { + err = _error; + if (!errors) { + errors = []; + } + errors.push({ + message: ['"', name, '" crashed on node ', this.type, ' No.', node.ID, ' (', node.board, ').'].join(''), + error: err + }); } } if (errors) { @@ -3242,13 +3240,16 @@ } Thread.prototype.setPage = function(pageNum) { - var icon, key, _i, _len, _ref; - icon = $('.page-num', this.OP.nodes.info); - _ref = ['title', 'textContent']; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - key = _ref[_i]; - icon[key] = icon[key].replace(/\d+/, pageNum); + var icon, info, quote, _ref; + _ref = this.OP.nodes, info = _ref.info, quote = _ref.quote; + if (!(icon = $('.page-num', info))) { + icon = $.el('span', { + className: 'page-num' + }); + $.after(quote, [$.tn(' '), icon]); } + icon.title = "This thread is on page " + pageNum + " in the original index."; + icon.textContent = "[" + pageNum + "]"; if (this.catalogView) { return this.catalogView.nodes.pageCount.textContent = pageNum; } @@ -3265,7 +3266,7 @@ }; Thread.prototype.setStatus = function(type, status) { - var name, typeLC; + var name; name = "is" + type; if (this[name] === status) { return; @@ -3274,7 +3275,6 @@ if (!this.OP) { return; } - typeLC = type.toLowerCase(); this.setIcon('Sticky', this.isSticky); this.setIcon('Closed', this.isClosed && !this.isArchived); return this.setIcon('Archived', this.isArchived); @@ -3301,7 +3301,7 @@ title: type, className: "" + typeLC + "Icon retina" }); - root = type !== 'Sticky' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : $('.page-num', this.OP.nodes.info) || $('[title="Reply to this post"]', this.OP.nodes.info); + root = type !== 'Sticky' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : $('.page-num', this.OP.nodes.info) || this.OP.nodes.quote; $.after(root, [$.tn(' '), icon]); if (!this.catalogView) { return; @@ -3375,11 +3375,12 @@ this.board = this.thread.board; this.nodes = { root: root, - thumb: $('.thumb', root), - icons: $('.thread-icons', root), + thumb: $('.catalog-thumb', root), + icons: $('.catalog-icons', root), postCount: $('.post-count', root), fileCount: $('.file-count', root), - pageCount: $('.page-count', root) + pageCount: $('.page-count', root), + comment: $('.comment', root) }; this.thread.catalogView = this; } @@ -3404,17 +3405,18 @@ } this.ID = +root.id.slice(2); this.fullID = "" + this.board + "." + this.ID; - post = $('.post', root); - info = $('.postInfo', post); if (that.isOriginalMarkup) { this.cleanup(root, post); } + post = $('.post', root); + info = $('.postInfo', post); root.dataset.fullID = this.fullID; this.nodes = { root: root, post: post, info: info, nameBlock: $('.nameBlock', info), + quote: $('.postNum > a:nth-of-type(2)', info), comment: $('.postMessage', post), links: [], quotelinks: [], @@ -3422,8 +3424,12 @@ }; if (!(this.isReply = $.hasClass(post, 'reply'))) { this.thread.OP = this; + this.thread.isArchived = !!$('.archivedIcon', info); this.thread.isSticky = !!$('.stickyIcon', info); - this.thread.isClosed = !!$('.closedIcon', info); + this.thread.isClosed = this.thread.isArchived || !!$('.closedIcon', info); + if (this.thread.isArchived) { + this.thread.kill(); + } } this.info = {}; this.info.nameBlock = Conf['Anonymize'] ? 'Anonymous' : this.nodes.nameBlock.textContent.trim(); @@ -3474,27 +3480,45 @@ } Post.prototype.parseComment = function() { - var bq, i, node, nodes, text; + var bq, i, node, nodes, spoilers; this.nodes.comment.normalize(); bq = this.nodes.comment.cloneNode(true); - nodes = $$('.abbr, .exif, b', bq); + nodes = $$('.abbr, .exif, b, marquee', bq); i = 0; while (node = nodes[i++]) { $.rm(node); } + this.info.comment = this.nodesToText(bq); + spoilers = $$('s', bq); + return this.info.commentSpoilered = (function() { + var _i, _len; + if (spoilers.length) { + for (_i = 0, _len = spoilers.length; _i < _len; _i++) { + node = spoilers[_i]; + $.replace(node, $.tn('[spoiler]')); + } + return this.nodesToText(bq); + } else { + return this.info.comment; + } + }).call(this); + }; + + Post.prototype.nodesToText = function(bq) { + var i, node, nodes, text; text = ""; nodes = $.X('.//br|.//text()', bq); i = 0; while (node = nodes.snapshotItem(i++)) { text += node.data || '\n'; } - return this.info.comment = text.trim().replace(/\s+$/gm, ''); + return text.trim().replace(/\s+$/gm, ''); }; Post.prototype.parseQuotes = function() { var quotelink, _i, _len, _ref; this.quotes = []; - _ref = $$('.quotelink', this.nodes.comment); + _ref = $$(':not(pre) > .quotelink', this.nodes.comment); for (_i = 0, _len = _ref.length; _i < _len; _i++) { quotelink = _ref[_i]; this.parseQuote(quotelink); @@ -3503,7 +3527,7 @@ Post.prototype.parseQuote = function(quotelink) { var fullID, match; - if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/thread\/\d+.*\#p(\d+)$/))) { + if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/(?:res|thread)\/\d+(?:\/[^#]*)?#p(\d+)$/))) { return; } this.nodes.quotelinks.push(quotelink); @@ -3517,7 +3541,7 @@ }; Post.prototype.parseFile = function(that) { - var anchor, fileEl, fileText, nameNode, size, thumb, unit, _ref; + var anchor, fileEl, fileText, nameNode, size, thumb, unit; if (!((fileEl = $('.file', this.nodes.post)) && (thumb = $('img[data-md5]', fileEl)))) { return; } @@ -3537,31 +3561,26 @@ size *= 1024; } this.file.sizeInBytes = size; - this.file.thumbURL = that.isArchived ? thumb.src : "" + location.protocol + "//t.4cdn.org/" + this.board + "/" + (this.file.URL.match(/(\d+)\./)[1]) + "s.jpg"; + this.file.thumbURL = "" + location.protocol + "//t.4cdn.org/" + this.board + "/" + (this.file.URL.match(/(\d+)\./)[1]) + "s.jpg"; this.file.isImage = /(jpg|png|gif)$/i.test(this.file.URL); this.file.isVideo = /webm$/i.test(this.file.URL); + nameNode = $('a', fileText); if (this.file.isImage || this.file.isVideo) { - this.file.dimensions = fileText.childNodes[2].data.match(/\d+x\d+/)[0]; + this.file.dimensions = nameNode.nextSibling.textContent.match(/\d+x\d+/)[0]; } - this.file.name = !this.file.isSpoiler && (nameNode = $('a', fileText)) ? nameNode.title || nameNode.textContent : fileText.title; - return this.file.name = (_ref = this.file.name) != null ? _ref.replace(/%22/g, '"') : void 0; + return this.file.name = fileText.title || nameNode.title || nameNode.textContent; }; - Post.prototype.cleanup = function(root, post) { - var node, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2; + Post.prototype.cleanup = function(root) { + var node, _i, _j, _len, _len1, _ref, _ref1; _ref = $$('.mobile', root); for (_i = 0, _len = _ref.length; _i < _len; _i++) { node = _ref[_i]; $.rm(node); } - _ref1 = $$('[id]:not(.exif)', post); + _ref1 = $$('.desktop', root); for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { node = _ref1[_j]; - node.removeAttribute('id'); - } - _ref2 = $$('.desktop', root); - for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { - node = _ref2[_k]; $.rmClass(node, 'desktop'); } }; @@ -3633,9 +3652,6 @@ return; } this.isHidden = false; - this.labels = this.labels.filter(function(label) { - return !/^(Manually hidden|Recursively hidden|Hidden by)/.test(label); - }); _ref = Get.allQuotelinksLinkingTo(this); for (_i = 0, _len = _ref.length; _i < _len; _i++) { quotelink = _ref[_i]; @@ -3664,8 +3680,7 @@ return delete this.nodes.stub; }; - Post.prototype.highlight = function(label, highlight, top) { - this.labels.push(label); + Post.prototype.highlight = function(highlight, top) { if (__indexOf.call(this.highlights, highlight) < 0) { this.highlights.push(highlight); $.addClass(this.nodes.root, highlight); @@ -3778,7 +3793,7 @@ __extends(Clone, _super); function Clone(origin, context, contractThumb) { - var file, info, inline, inlined, key, nodes, post, root, val, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + var file, info, inline, inlined, key, nodes, post, root, val, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3; this.origin = origin; this.context = context; _ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']; @@ -3787,13 +3802,15 @@ this[key] = origin[key]; } nodes = origin.nodes; - root = nodes.root.cloneNode(true); + root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true); post = $('.post', root); info = $('.postInfo', post); this.nodes = { root: root, post: post, info: info, + nameBlock: $('.nameBlock', info), + quote: $('.postNum > a:nth-of-type(2)', info), comment: $('.postMessage', post), quotelinks: [], backlinks: info.getElementsByClassName('backlink') @@ -3827,10 +3844,10 @@ this.nodes.uniqueID = $('.posteruid', info); } if (nodes.capcode) { - this.nodes.capcode = $('.capcode', info); + this.nodes.capcode = $('.capcode.hand', info); } if (nodes.flag) { - this.nodes.flag = $('.countryFlag', info); + this.nodes.flag = $('.flag, .countryFlag', info); } if (nodes.date) { this.nodes.date = $('.dateTime', info); @@ -3845,18 +3862,11 @@ } file = $('.file', post); this.file.text = file.firstElementChild; - this.file.thumb = $('img[data-md5], video[data-md5]', file); + this.file.thumb = $('.fileThumb > [data-md5]', file); this.file.fullImage = $('.full-image', file); + this.file.videoControls = $('.video-controls', this.file.text); if (contractThumb) { - $.rmClass(root, 'expanded-image'); - $.rmClass(this.file.thumb, 'expanding'); - } - this.file.isExpanded = $.hasClass(root, 'expanded-image'); - if ((_ref4 = this.file.fullImage) != null) { - _ref4.removeAttribute('id'); - } - if ((_ref5 = $('.video-controls', this.file.text)) != null) { - _ref5.remove(); + ImageExpand.contract(this); } } if (origin.isDead) { @@ -3866,6 +3876,23 @@ root.dataset.clone = origin.clones.push(this) - 1; } + Clone.prototype.cloneWithoutVideo = function(node) { + var child, clone, _i, _len, _ref; + if (node.tagName === 'VIDEO' && !node.dataset.md5) { + return []; + } else if (node.nodeType === Node.ELEMENT_NODE && $('video', node)) { + clone = node.cloneNode(false); + _ref = node.childNodes; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + $.add(clone, this.cloneWithoutVideo(child)); + } + return clone; + } else { + return node.cloneNode(true); + } + }; + return Clone; })(Post); @@ -4039,12 +4066,13 @@ })(); Notice = (function() { - function Notice(type, content, timeout) { + function Notice(type, content, timeout, onclose) { this.timeout = timeout; + this.onclose = onclose; this.close = __bind(this.close, this); this.add = __bind(this.add, this); this.el = $.el('div', { - innerHTML: '\uf00d
' + innerHTML: "
" }); this.el.style.opacity = 0; this.setType(type); @@ -4076,7 +4104,8 @@ Notice.prototype.close = function() { $.off(d, 'visibilitychange', this.add); - return $.rm(this.el); + $.rm(this.el); + return typeof this.onclose === "function" ? this.onclose() : void 0; }; return Notice; @@ -4116,7 +4145,7 @@ RandomAccessList.prototype.before = function(root, item) { var prev; - if (item.next === root) { + if (item.next === root || item === root) { return; } this.rmi(item); @@ -4126,6 +4155,8 @@ item.prev = prev; if (prev) { return prev.next = item; + } else { + return this.first = item; } }; @@ -4141,6 +4172,8 @@ item.next = next; if (next) { return next.prev = item; + } else { + return this.last = item; } }; @@ -4152,7 +4185,11 @@ } this.rmi(item); item.next = first; - first.prev = item; + if (first) { + first.prev = item; + } else { + this.last = item; + } this.first = item; return delete item.prev; }; @@ -4307,7 +4344,26 @@ Polyfill = { init: function() { - return this.toBlob(); + this.notificationPermission(); + this.toBlob(); + return this.visibility(); + }, + notificationPermission: function() { + if (!window.Notification || 'permission' in Notification || !window.webkitNotifications) { + return; + } + return Object.defineProperty(Notification, 'permission', { + get: function() { + switch (webkitNotifications.checkPermission()) { + case 0: + return 'granted'; + case 1: + return 'default'; + case 2: + return 'denied'; + } + } + }); }, toBlob: function() { var _base; @@ -4323,6 +4379,26 @@ type: 'image/png' })); }); + }, + visibility: function() { + if ('visibilityState' in d) { + return; + } + Object.defineProperties(HTMLDocument.prototype, { + visibilityState: { + get: function() { + return this.webkitVisibilityState; + } + }, + hidden: { + get: function() { + return this.webkitHidden; + } + } + }); + return $.on(d, 'webkitvisibilitychange', function() { + return $.event('visibilitychange'); + }); } }; @@ -13469,7 +13545,7 @@ MarkNewIPs.ipCount = this.ipCount; MarkNewIPs.postIDs = (function() { var _i, _len, _ref, _results; - _ref = this.post.keys; + _ref = this.posts.keys; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { x = _ref[_i]; diff --git a/src/General/lib/callbacks.class b/src/General/lib/callbacks.class index 7a5225c87..0fc2218fe 100644 --- a/src/General/lib/callbacks.class +++ b/src/General/lib/callbacks.class @@ -11,21 +11,13 @@ class Callbacks disconnect: (name) -> @[name].disconnected = true if @[name] execute: (nodes) -> - i = 0 - # c.time 'Features' - while name = @keys[i++] - j = 0 - cb = @[name] - # c.time name - while node = nodes[j++] - try - cb.call node unless cb.disconnected - catch err - errors = [] unless errors - errors.push - message: ['"', name, '" crashed on node ', @type, ' No.', node.ID, ' (', node.board, ').'].join('') - error: err - # c.timeEnd name - # c.timeEnd 'Features' + for name in @keys + try + @[name].call node for node in nodes + catch err + errors = [] unless errors + errors.push + message: ['"', name, '" crashed on node ', @type, ' No.', node.ID, ' (', node.board, ').'].join('') + error: err Main.handleErrors errors if errors diff --git a/src/General/lib/catalogthread.class b/src/General/lib/catalogthread.class index 45d6e99ea..40f6fcd94 100644 --- a/src/General/lib/catalogthread.class +++ b/src/General/lib/catalogthread.class @@ -7,9 +7,10 @@ class CatalogThread @board = @thread.board @nodes = root: root - thumb: $ '.thumb', root - icons: $ '.thread-icons', root - postCount: $ '.post-count', root - fileCount: $ '.file-count', root - pageCount: $ '.page-count', root + thumb: $ '.catalog-thumb', root + icons: $ '.catalog-icons', root + postCount: $ '.post-count', root + fileCount: $ '.file-count', root + pageCount: $ '.page-count', root + comment: $ '.comment', root @thread.catalogView = @ diff --git a/src/General/lib/classes.coffee b/src/General/lib/classes.coffee index 7f579bf6d..eeb3095e5 100755 --- a/src/General/lib/classes.coffee +++ b/src/General/lib/classes.coffee @@ -9,4 +9,4 @@ <%= grunt.file.read('src/General/lib/randomaccesslist.class') %> <%= grunt.file.read('src/General/lib/simpledict.class') %> <%= grunt.file.read('src/General/lib/set.class') %> -<%= grunt.file.read('src/General/lib/connection.class') %> \ No newline at end of file +<%= grunt.file.read('src/General/lib/connection.class') %> diff --git a/src/General/lib/clone.class b/src/General/lib/clone.class index 8f40c7dbc..a6fc02366 100755 --- a/src/General/lib/clone.class +++ b/src/General/lib/clone.class @@ -5,13 +5,18 @@ class Clone extends Post @[key] = origin[key] {nodes} = origin - root = nodes.root.cloneNode true + root = if contractThumb + @cloneWithoutVideo nodes.root + else + nodes.root.cloneNode true post = $ '.post', root info = $ '.postInfo', post @nodes = root: root post: post info: info + nameBlock: $ '.nameBlock', info + quote: $ '.postNum > a:nth-of-type(2)', info comment: $ '.postMessage', post quotelinks: [] backlinks: info.getElementsByClassName 'backlink' @@ -37,9 +42,9 @@ class Clone extends Post if nodes.uniqueID @nodes.uniqueID = $ '.posteruid', info if nodes.capcode - @nodes.capcode = $ '.capcode', info + @nodes.capcode = $ '.capcode.hand', info if nodes.flag - @nodes.flag = $ '.countryFlag', info + @nodes.flag = $ '.flag, .countryFlag', info if nodes.date @nodes.date = $ '.dateTime', info @@ -53,21 +58,23 @@ class Clone extends Post @file[key] = val file = $ '.file', post @file.text = file.firstElementChild - @file.thumb = $ 'img[data-md5], video[data-md5]', file + @file.thumb = $ '.fileThumb > [data-md5]', file @file.fullImage = $ '.full-image', file + @file.videoControls = $ '.video-controls', @file.text # Contract thumbnails in quote preview - if contractThumb - $.rmClass root, 'expanded-image' - $.rmClass @file.thumb, 'expanding' - @file.isExpanded = $.hasClass root, 'expanded-image' - - # Remove any #ihover ID - @file.fullImage?.removeAttribute 'id' - - # Remove video controls. - ($ '.video-controls', @file.text)?.remove() + ImageExpand.contract @ if contractThumb @isDead = true if origin.isDead @isClone = true - root.dataset.clone = origin.clones.push(@) - 1 \ No newline at end of file + root.dataset.clone = origin.clones.push(@) - 1 + + cloneWithoutVideo: (node) -> + if node.tagName is 'VIDEO' and !node.dataset.md5 # (exception for WebM thumbnails) + [] + else if node.nodeType is Node.ELEMENT_NODE and $ 'video', node + clone = node.cloneNode false + $.add clone, @cloneWithoutVideo child for child in node.childNodes + clone + else + node.cloneNode true diff --git a/src/General/lib/notice.class b/src/General/lib/notice.class index 4eae06879..9c371dd25 100644 --- a/src/General/lib/notice.class +++ b/src/General/lib/notice.class @@ -1,7 +1,7 @@ class Notice - constructor: (type, content, @timeout) -> + constructor: (type, content, @timeout, @onclose) -> @el = $.el 'div', - innerHTML: '\uf00d
' + <%= html('
') %> @el.style.opacity = 0 @setType type $.on @el.firstElementChild, 'click', @close @@ -27,3 +27,4 @@ class Notice close: => $.off d, 'visibilitychange', @add $.rm @el + @onclose?() diff --git a/src/General/lib/polyfill.coffee b/src/General/lib/polyfill.coffee index e09a458b0..6f7799f06 100755 --- a/src/General/lib/polyfill.coffee +++ b/src/General/lib/polyfill.coffee @@ -1,8 +1,19 @@ Polyfill = init: -> - <% if (type === 'crx') { %> + @notificationPermission() @toBlob() - <% } %> + @visibility() + notificationPermission: -> + return if !window.Notification or 'permission' of Notification or !window.webkitNotifications + Object.defineProperty Notification, 'permission', + get: -> + switch webkitNotifications.checkPermission() + when 0 + 'granted' + when 1 + 'default' + when 2 + 'denied' toBlob: -> HTMLCanvasElement::toBlob or= (cb) -> data = atob @toDataURL()[22..] @@ -12,3 +23,12 @@ Polyfill = for i in [0...l] by 1 ui8a[i] = data.charCodeAt i cb new Blob [ui8a], type: 'image/png' + visibility: -> + # page visibility API + return if 'visibilityState' of d + Object.defineProperties HTMLDocument.prototype, + visibilityState: + get: -> @webkitVisibilityState + hidden: + get: -> @webkitHidden + $.on d, 'webkitvisibilitychange', -> $.event 'visibilitychange' diff --git a/src/General/lib/post.class b/src/General/lib/post.class index 957a01ccf..58ed72792 100755 --- a/src/General/lib/post.class +++ b/src/General/lib/post.class @@ -6,15 +6,16 @@ class Post @ID = +root.id[2..] @fullID = "#{@board}.#{@ID}" + @cleanup root, post if that.isOriginalMarkup post = $ '.post', root info = $ '.postInfo', post - @cleanup root, post if that.isOriginalMarkup root.dataset.fullID = @fullID @nodes = root: root post: post info: info nameBlock: $ '.nameBlock', info + quote: $ '.postNum > a:nth-of-type(2)', info comment: $ '.postMessage', post links: [] quotelinks: [] @@ -22,8 +23,10 @@ class Post unless @isReply = $.hasClass post, 'reply' @thread.OP = @ - @thread.isSticky = !!$ '.stickyIcon', info - @thread.isClosed = !!$ '.closedIcon', info + @thread.isArchived = !!$ '.archivedIcon', info + @thread.isSticky = !!$ '.stickyIcon', info + @thread.isClosed = @thread.isArchived or !!$ '.closedIcon', info + @thread.kill() if @thread.isArchived @info = {} @info.nameBlock = if Conf['Anonymize'] @@ -71,28 +74,44 @@ class Post parseComment: -> # Merge text nodes and remove empty ones. @nodes.comment.normalize() + # Get the comment's text. #
-> \n # Remove: + # 'Comment too long'... # EXIF data. (/p/) # Rolls. (/tg/) + # Marquees. (/pol/) # Preceding and following new lines. # Trailing spaces. bq = @nodes.comment.cloneNode true - nodes = $$ '.abbr, .exif, b', bq + nodes = $$ '.abbr, .exif, b, marquee', bq i = 0 - while node = nodes[i++] - $.rm node + $.rm node while node = nodes[i++] + @info.comment = @nodesToText bq + + # Get the comment's text with spoilers hidden. + spoilers = $$ 's', bq + @info.commentSpoilered = if spoilers.length + for node in spoilers + $.replace node, $.tn '[spoiler]' + @nodesToText bq + else + @info.comment + + nodesToText: (bq) -> text = "" nodes = $.X './/br|.//text()', bq i = 0 while node = nodes.snapshotItem i++ text += node.data or '\n' - @info.comment = text.trim().replace /\s+$/gm, '' + text.trim().replace /\s+$/gm, '' parseQuotes: -> @quotes = [] - for quotelink in $$ '.quotelink', @nodes.comment + # XXX https://github.com/4chan/4chan-JS/issues/77 + # 4chan currently creates quote links inside [code] tags; ignore them + for quotelink in $$ ':not(pre) > .quotelink', @nodes.comment @parseQuote quotelink return @@ -106,9 +125,8 @@ class Post return unless match = quotelink.href.match /// boards\.4chan\.org/ ([^/]+) # boardID - /thread/\d+ - .* # thread slug - \#p(\d+) # postID + /(?:res|thread)/\d+(?:\/[^#]*)?#p + (\d+) # postID $ /// @@ -122,7 +140,7 @@ class Post parseFile: (that) -> return unless (fileEl = $ '.file', @nodes.post) and thumb = $ 'img[data-md5]', fileEl - # Supports JPG/PNG/GIF/WEBM/PDF. + # Supports JPG/PNG/GIF/PDF. # Flash files are not supported. anchor = thumb.parentNode fileText = fileEl.firstElementChild @@ -137,32 +155,17 @@ class Post unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0] size *= 1024 while unit-- > 0 @file.sizeInBytes = size - @file.thumbURL = if that.isArchived - thumb.src - else - "#{location.protocol}//t.4cdn.org/#{@board}/#{@file.URL.match(/(\d+)\./)[1]}s.jpg" + @file.thumbURL = "#{location.protocol}//t.4cdn.org/#{@board}/#{@file.URL.match(/(\d+)\./)[1]}s.jpg" @file.isImage = /(jpg|png|gif)$/i.test @file.URL @file.isVideo = /webm$/i.test @file.URL + nameNode = $ 'a', fileText if @file.isImage or @file.isVideo - @file.dimensions = fileText.childNodes[2].data.match(/\d+x\d+/)[0] - @file.name = if !@file.isSpoiler and nameNode = $ 'a', fileText - nameNode.title or nameNode.textContent - else - fileText.title - <% if (type === 'crx') { %> - # replace %22 with quotes, see: - # crbug.com/81193 - # webk.it/62107 - # https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909 - # http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data - @file.name = @file.name?.replace /%22/g, '"' - <% } %> + @file.dimensions = nameNode.nextSibling.textContent.match(/\d+x\d+/)[0] + @file.name = fileText.title or nameNode.title or nameNode.textContent - cleanup: (root, post) -> + cleanup: (root) -> for node in $$ '.mobile', root $.rm node - for node in $$ '[id]:not(.exif)', post - node.removeAttribute 'id' for node in $$ '.desktop', root $.rmClass node, 'desktop' return @@ -209,9 +212,6 @@ class Post show: (showRecursively=Conf['Recursive Hiding']) -> return if !@isHidden @isHidden = false - @labels = @labels.filter (label) -> - # This is lame. - !/^(Manually hidden|Recursively hidden|Hidden by)/.test label for quotelink in Get.allQuotelinksLinkingTo @ $.rmClass quotelink, 'filtered' @@ -233,8 +233,7 @@ class Post @nodes.post.previousElementSibling.hidden = false $.rm @nodes.stub delete @nodes.stub - highlight: (label, highlight, top) -> - @labels.push label + highlight: (highlight, top) -> unless highlight in @highlights @highlights.push highlight $.addClass @nodes.root, highlight diff --git a/src/General/lib/randomaccesslist.class b/src/General/lib/randomaccesslist.class index c341a4163..de027feb6 100644 --- a/src/General/lib/randomaccesslist.class +++ b/src/General/lib/randomaccesslist.class @@ -21,7 +21,7 @@ class RandomAccessList @length++ before: (root, item) -> - return if item.next is root + return if item.next is root or item is root @rmi item @@ -29,7 +29,10 @@ class RandomAccessList root.prev = item item.next = root item.prev = prev - prev.next = item if prev + if prev + prev.next = item + else + @first = item after: (root, item) -> return if item.prev is root @@ -40,14 +43,20 @@ class RandomAccessList root.next = item item.prev = root item.next = next - next.prev = item if next + if next + next.prev = item + else + @last = item prepend: (item) -> {first} = @ return if item is first or not @[item.ID] @rmi item item.next = first - first.prev = item + if first + first.prev = item + else + @last = item @first = item delete item.prev @@ -77,4 +86,4 @@ class RandomAccessList if next next.prev = prev else - @last = prev \ No newline at end of file + @last = prev diff --git a/src/General/lib/thread.class b/src/General/lib/thread.class index ba61ccdc1..5e275bcc7 100755 --- a/src/General/lib/thread.class +++ b/src/General/lib/thread.class @@ -22,24 +22,28 @@ class Thread g.threads.push @fullID, board.threads.push @, @ setPage: (pageNum) -> - icon = $ '.page-num', @OP.nodes.info - for key in ['title', 'textContent'] - icon[key] = icon[key].replace /\d+/, pageNum + {info, quote} = @OP.nodes + unless icon = $ '.page-num', info + icon = $.el 'span', className: 'page-num' + $.after quote, [$.tn(' '), icon] + icon.title = "This thread is on page #{pageNum} in the original index." + icon.textContent = "[#{pageNum}]" @catalogView.nodes.pageCount.textContent = pageNum if @catalogView + setCount: (type, count, reachedLimit) -> return unless @catalogView el = @catalogView.nodes["#{type}Count"] el.textContent = count (if reachedLimit then $.addClass else $.rmClass) el, 'warning' + setStatus: (type, status) -> name = "is#{type}" return if @[name] is status @[name] = status return unless @OP - typeLC = type.toLowerCase() - @setIcon 'Sticky', @isSticky - @setIcon 'Closed', @isClosed and !@isArchived + @setIcon 'Sticky', @isSticky + @setIcon 'Closed', @isClosed and !@isArchived @setIcon 'Archived', @isArchived setIcon: (type, status) -> @@ -58,11 +62,10 @@ class Thread title: type className: "#{typeLC}Icon retina" - root = - if type isnt 'Sticky' and @isSticky - $ '.stickyIcon', @OP.nodes.info - else - $('.page-num', @OP.nodes.info) or $('[title="Reply to this post"]', @OP.nodes.info) + root = if type isnt 'Sticky' and @isSticky + $ '.stickyIcon', @OP.nodes.info + else + $('.page-num', @OP.nodes.info) or @OP.nodes.quote $.after root, [$.tn(' '), icon] return unless @catalogView @@ -71,6 +74,7 @@ class Thread pin: -> @isPinned = true $.addClass @catalogView.nodes.root, 'pinned' if @catalogView + unpin: -> @isPinned = false $.rmClass @catalogView.nodes.root, 'pinned' if @catalogView @@ -81,6 +85,7 @@ class Thread @OP.nodes.root.parentElement.hidden = true if button = $ '.hide-post-button', @OP.nodes.root $.replace button, PostHiding.makeButton false + show: -> return if !@isHidden @isHidden = false diff --git a/src/Monitoring/MarkNewIPs.coffee b/src/Monitoring/MarkNewIPs.coffee index 58c4b5975..81c6ed4a9 100644 --- a/src/Monitoring/MarkNewIPs.coffee +++ b/src/Monitoring/MarkNewIPs.coffee @@ -7,7 +7,7 @@ MarkNewIPs = node: -> MarkNewIPs.ipCount = @ipCount - MarkNewIPs.postIDs = (+x for x in @post.keys) + MarkNewIPs.postIDs = (+x for x in @posts.keys) $.on d, 'ThreadUpdate', MarkNewIPs.onUpdate onUpdate: (e) ->