diff --git a/CHANGELOG.md b/CHANGELOG.md index c2be58cff..d1e02172c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ The attributions below are for work that has been incorporated into the script a The links to individual versions below are to copies of the script with the update URL removed. If you want automatic updates, install the script from the links on the [main page](https://github.com/ccd0/4chan-x). + +### v1.9.14.0 +*2014-11-30* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.9.14.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.9.14.0/builds/4chan-X-noupdate.crx "Chromium version")] + +**ccd0** +- `Quote Threading` is now compatible with the `Unread Line` and `Scroll to Last Read Post` features. +- When there are unread replies to a read post, which are not threaded so you don't miss them, a `[Thread New Posts]` link appears at the bottom of the page, which will thread them when clicked. +- Fix some scrolling bugs when images are contracted. + ### v1.9.13.4 *2014-11-26* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.9.13.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.9.13.4/builds/4chan-X-noupdate.crx "Chromium version")] diff --git a/LICENSE b/LICENSE index 4a7672452..ac4498697 100755 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ /* -* 4chan X - Version 1.9.13.4 +* 4chan X - Version 1.9.14.0 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE diff --git a/builds/4chan-X-beta.crx b/builds/4chan-X-beta.crx index 8cb63edb0..afee6d5a2 100644 Binary files a/builds/4chan-X-beta.crx and b/builds/4chan-X-beta.crx differ diff --git a/builds/4chan-X-beta.meta.js b/builds/4chan-X-beta.meta.js index bc015f03d..593220454 100644 --- a/builds/4chan-X-beta.meta.js +++ b/builds/4chan-X-beta.meta.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X beta -// @version 1.9.13.4 +// @version 1.9.14.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X diff --git a/builds/4chan-X-beta.user.js b/builds/4chan-X-beta.user.js index dc737afdf..5d3eda37a 100644 --- a/builds/4chan-X-beta.user.js +++ b/builds/4chan-X-beta.user.js @@ -1,7 +1,7 @@ // Generated by CoffeeScript // ==UserScript== // @name 4chan X beta -// @version 1.9.13.4 +// @version 1.9.14.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X @@ -24,7 +24,7 @@ // ==/UserScript== /* -* 4chan X - Version 1.9.13.4 +* 4chan X - Version 1.9.14.0 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE @@ -107,7 +107,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, MarkNewIPs, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, + var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, MarkNewIPs, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, ShimSet, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -388,7 +388,7 @@ doc = d.documentElement; g = { - VERSION: '1.9.13.4', + VERSION: '1.9.14.0', NAMESPACE: '4chan X.', NAME: '4chan X', FAQ: 'https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions', @@ -1887,6 +1887,40 @@ })(); + ShimSet = (function() { + function ShimSet() { + this.elements = {}; + this.size = 0; + } + + ShimSet.prototype.has = function(value) { + return value in this.elements; + }; + + ShimSet.prototype.add = function(value) { + if (this.elements[value]) { + return; + } + this.elements[value] = true; + return this.size++; + }; + + ShimSet.prototype["delete"] = function(value) { + if (!this.elements[value]) { + return; + } + delete this.elements[value]; + return this.size--; + }; + + return ShimSet; + + })(); + + if (!('Set' in window)) { + window.Set = ShimSet; + } + Polyfill = { init: function() { this.notificationPermission(); @@ -6024,7 +6058,6 @@ QuoteThreading = { init: function() { - var input; if (!(Conf['Quote Threading'] && g.VIEW === 'thread')) { return; } @@ -6032,123 +6065,164 @@ this.controls = $.el('span', { innerHTML: "" }); - input = $('input', this.controls); - $.on(input, 'change', this.toggle); + this.threadNewLink = $.el('span', { + className: 'brackets-wrap threadnewlink', + hidden: true + }); + $.extend(this.threadNewLink, { + innerHTML: "Thread New Posts" + }); + $.on($('input', this.controls), 'change', function() { + return QuoteThreading.rethread(this.checked); + }); + $.on(this.threadNewLink.firstElementChild, 'click', function() { + QuoteThreading.threadNewLink.hidden = true; + return QuoteThreading.rethread(true); + }); Header.menu.addEntry(this.entry = { el: this.controls, order: 98 }); + Thread.callbacks.push({ + name: 'Quote Threading', + cb: this.setThread + }); return Post.callbacks.push({ name: 'Quote Threading', cb: this.node }); }, - force: function() { - g.posts.forEach(function(post) { - if (post.cb) { - return post.cb(true); - } + parent: {}, + children: {}, + inserted: {}, + setThread: function() { + QuoteThreading.thread = this; + return $.asap((function() { + return !Conf['Thread Updater'] || $('.navLinksBot > .updatelink'); + }), function() { + return $.add($('.navLinksBot'), [$.tn(' '), QuoteThreading.threadNewLink]); }); - Unread.read(); - return Unread.update(); }, node: function() { - var keys, len, posts, quote, _i, _len, _ref; - posts = g.posts; - if (this.isClone || !QuoteThreading.enabled) { + var parent, parents, quote, thread; + if (this.isFetchedQuote || this.isClone || !this.isReply) { return; } - Unread.addPost(this); - if (this.thread.OP === this || this.isHidden) { - return; + thread = QuoteThreading.thread; + parents = (function() { + var _i, _len, _ref, _results; + _ref = this.quotes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + quote = _ref[_i]; + parent = g.posts[quote]; + if (!parent || parent.isFetchedQuote || !parent.isReply || parent.ID >= this.ID) { + continue; + } + _results.push(parent); + } + return _results; + }).call(this); + if (parents.length === 1) { + return QuoteThreading.parent[this.fullID] = parents[0]; } - keys = []; - len = g.BOARD.ID.length + 1; - _ref = this.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if ((quote.slice(len) < this.ID) && quote in posts) { - keys.push(quote); + }, + descendants: function(post) { + var child, children, posts, _i, _len; + posts = [post]; + if (children = QuoteThreading.children[post.fullID]) { + for (_i = 0, _len = children.length; _i < _len; _i++) { + child = children[_i]; + posts = posts.concat(QuoteThreading.descendants(child)); } } - if (keys.length !== 1) { - return; - } - this.threaded = keys[0]; - return this.cb = QuoteThreading.nodeinsert; + return posts; }, - nodeinsert: function(force) { - var bottom, height, post, posts, root, threadContainer, top, _ref; - post = g.posts[this.threaded]; - if (this.thread.OP === post) { + insert: function(post) { + var child, children, descendants, i, next, nodes, order, parent, prev, prev2, threadContainer, x, _base, _i, _j, _k, _len, _name; + if (!(QuoteThreading.enabled && (parent = QuoteThreading.parent[post.fullID]) && !QuoteThreading.inserted[post.fullID])) { return false; } - posts = Unread.posts; - root = post.nodes.root; - if (!force) { - height = doc.clientHeight; - _ref = root.getBoundingClientRect(), bottom = _ref.bottom, top = _ref.top; - if (!(posts[post.ID] || ((bottom < height) && (top > 0)))) { - return false; + descendants = QuoteThreading.descendants(post); + if (!Unread.posts.has(parent.ID) && descendants.some(function(x) { + return Unread.posts.has(x.ID); + })) { + QuoteThreading.threadNewLink.hidden = false; + return false; + } + order = Unread.order; + children = ((_base = QuoteThreading.children)[_name = parent.fullID] || (_base[_name] = [])); + threadContainer = parent.nodes.threadContainer || $.el('div', { + className: 'threadContainer' + }); + nodes = [post.nodes.root]; + if (post.nodes.threadContainer) { + nodes.push(post.nodes.threadContainer); + } + i = children.length; + for (_i = children.length - 1; _i >= 0; _i += -1) { + child = children[_i]; + if (child.ID >= post.ID) { + i--; } } - if ($.hasClass(root, 'threadOP')) { - threadContainer = root.nextElementSibling; - post = Get.postFromRoot($.x('descendant::div[contains(@class,"postContainer")][last()]', threadContainer)); - $.add(threadContainer, this.nodes.root); + if (i !== children.length) { + next = children[i]; + for (_j = 0, _len = descendants.length; _j < _len; _j++) { + x = descendants[_j]; + order.before(order[next.ID], order[x.ID]); + } + children.splice(i, 0, post); + $.before(next.nodes.root, nodes); } else { - threadContainer = $.el('div', { - className: 'threadContainer' - }); - $.add(threadContainer, this.nodes.root); - $.after(root, threadContainer); - $.addClass(root, 'threadOP'); + prev = parent; + while ((prev2 = QuoteThreading.children[prev.fullID]) && prev2.length) { + prev = prev2[prev2.length - 1]; + } + for (_k = descendants.length - 1; _k >= 0; _k += -1) { + x = descendants[_k]; + order.after(order[prev.ID], order[x.ID]); + } + children.push(post); + $.add(threadContainer, nodes); } - if (post = posts[post.ID]) { - posts.after(post, posts[this.ID]); - } else if (posts[this.ID]) { - posts.prepend(posts[this.ID]); + QuoteThreading.inserted[post.fullID] = true; + if (!parent.nodes.threadContainer) { + parent.nodes.threadContainer = threadContainer; + $.addClass(parent.nodes.root, 'threadOP'); + $.after(parent.nodes.root, threadContainer); } return true; }, - toggle: function() { - var container, containers, nodes, post, posts, thread, _i, _j, _k, _len, _len1, _len2, _ref; - if (QuoteThreading.enabled = this.checked) { - QuoteThreading.force(); + rethread: function(enabled) { + var nodes, posts, thread; + thread = QuoteThreading.thread; + posts = thread.posts; + if (QuoteThreading.enabled = enabled) { + posts.forEach(QuoteThreading.insert); } else { - thread = $('.thread'); - posts = []; nodes = []; - g.posts.forEach(function(post) { - if (!(post === post.thread.OP || post.isClone)) { - return posts.push(post); + Unread.order = new RandomAccessList; + QuoteThreading.inserted = {}; + posts.forEach(function(post) { + Unread.order.push(post); + if (post.isReply) { + nodes.push(post.nodes.root); + } + if (QuoteThreading.children[post.fullID]) { + delete QuoteThreading.children[post.fullID]; + $.rmClass(post.nodes.root, 'threadOP'); + $.rm(post.nodes.threadContainer); + return delete post.nodes.threadContainer; } }); - posts.sort(function(a, b) { - return a.ID - b.ID; - }); - for (_i = 0, _len = posts.length; _i < _len; _i++) { - post = posts[_i]; - nodes.push(post.nodes.root); - } - $.add(thread, nodes); - containers = $$('.threadContainer', thread); - for (_j = 0, _len1 = containers.length; _j < _len1; _j++) { - container = containers[_j]; - $.rm(container); - } - _ref = $$('.threadOP'); - for (_k = 0, _len2 = _ref.length; _k < _len2; _k++) { - post = _ref[_k]; - $.rmClass(post, 'threadOP'); - } + $.add(thread.OP.nodes.root.parentNode, nodes); } - }, - kb: function() { - var control; - control = $.id('threadingControl'); - control.checked = !control.checked; - return QuoteThreading.toggle.call(control); + Unread.position = Unread.order.first; + Unread.updatePosition(); + Unread.setLine(true); + Unread.read(); + return Unread.update(); } }; @@ -8862,44 +8936,34 @@ } }, toggle: function(post) { - var headRect, left, root, top, x, y, _ref; + var next; if (!(post.file.isExpanding || post.file.isExpanded)) { post.file.scrollIntoView = Conf['Scroll into view']; ImageExpand.expand(post); return; } ImageExpand.contract(post); - root = post.nodes.root; - _ref = (Conf['Advance on contract'] ? (function() { - var next; - next = root; + if (Conf['Advance on contract']) { + next = post.nodes.root; while (next = $.x("following::div[contains(@class,'postContainer')][1]", next)) { - if ($('.stub', next) || next.offsetHeight === 0) { - continue; + if (!($('.stub', next) || next.offsetHeight === 0)) { + break; } - return next; } - return root; - })() : root).getBoundingClientRect(), top = _ref.top, left = _ref.left; - if (top < 0) { - y = top; - if (Conf['Fixed Header'] && !Conf['Bottom Header']) { - headRect = Header.bar.getBoundingClientRect(); - y -= headRect.top + headRect.height; + if (next) { + return Header.scrollTo(next); } } - if (left < 0) { - x = -window.scrollX; - } - if (x || y) { - return window.scrollBy(x, y); - } }, contract: function(post) { - var bottom, cb, el, eventName, file, oldHeight, x, _i, _len, _ref, _ref1; + var bottom, cb, el, eventName, file, oldHeight, scrollY, top, x, _i, _len, _ref, _ref1; file = post.file; - bottom = post.nodes.root.getBoundingClientRect().bottom; - oldHeight = d.body.clientHeight; + if (el = file.fullImage) { + top = Header.getTopOf(el); + bottom = top + el.getBoundingClientRect().height; + oldHeight = d.body.clientHeight; + scrollY = window.scrollY; + } $.rmClass(post.nodes.root, 'expanded-image'); $.rmClass(file.thumb, 'expanding'); if (file.videoControls) { @@ -8912,35 +8976,43 @@ x = _ref[_i]; delete file[x]; } - if (doc.contains(post.nodes.root) && bottom <= 0) { - window.scrollBy(0, d.body.clientHeight - oldHeight); + if (!el) { + return; } - if (el = file.fullImage) { - $.off(el, 'error', ImageExpand.error); - ImageCommon.pushCache(el); - if (file.isVideo) { - el.pause(); - _ref1 = ImageExpand.videoCB; - for (eventName in _ref1) { - cb = _ref1[eventName]; - $.off(el, eventName, cb); - } + if (doc.contains(el)) { + if (bottom <= 0) { + window.scroll(0, scrollY + d.body.clientHeight - oldHeight); + } else { + Header.scrollToIfNeeded(post.nodes.root); } - if (Conf['Restart when Opened']) { - ImageCommon.rewind(file.thumb); + if (window.scrollX > 0) { + window.scroll(0, window.scrollY); } - delete file.fullImage; - return $.queueTask(function() { - if (file.isExpanding || file.isExpanded) { - return; - } - $.rmClass(el, 'full-image'); - if (el.id) { - return; - } - return $.rm(el); - }); } + $.off(el, 'error', ImageExpand.error); + ImageCommon.pushCache(el); + if (file.isVideo) { + el.pause(); + _ref1 = ImageExpand.videoCB; + for (eventName in _ref1) { + cb = _ref1[eventName]; + $.off(el, eventName, cb); + } + } + if (Conf['Restart when Opened']) { + ImageCommon.rewind(file.thumb); + } + delete file.fullImage; + return $.queueTask(function() { + if (file.isExpanding || file.isExpanded) { + return; + } + $.rmClass(el, 'full-image'); + if (el.id) { + return; + } + return $.rm(el); + }); }, expand: function(post, src) { var el, file, isVideo, thumb, _ref; @@ -8993,19 +9065,20 @@ } }, completeExpand: function(post) { - var bottom, file, imageBottom, oldHeight; + var bottom, file, imageBottom, oldHeight, scrollY; file = post.file; if (!file.isExpanding) { return; } - bottom = post.nodes.root.getBoundingClientRect().bottom; + bottom = Header.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; oldHeight = d.body.clientHeight; + scrollY = window.scrollY; $.addClass(post.nodes.root, 'expanded-image'); $.rmClass(file.thumb, 'expanding'); file.isExpanded = true; delete file.isExpanding; if (doc.contains(post.nodes.root) && bottom <= 0) { - window.scrollBy(0, d.body.clientHeight - oldHeight); + window.scroll(window.scrollX, scrollY + d.body.clientHeight - oldHeight); } if (file.scrollIntoView) { delete file.scrollIntoView; @@ -9207,7 +9280,7 @@ ImageCommon.rewind(this); } el.id = 'ihover'; - $.after(Header.hover, el); + $.add(Header.hover, el); if (isVideo) { el.loop = true; el.controls = false; @@ -11143,7 +11216,7 @@ return new Notice('info', "The thread is " + change + ".", 30); }, parse: function(postObjects) { - var OP, count, files, index, ipCountEl, node, num, post, postObject, posts, root, scroll, _i, _j, _len, _len1; + var OP, count, files, index, ipCountEl, node, num, post, postObject, posts, scroll, _i, _j, _len, _len1; OP = postObjects[0]; Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler; ThreadUpdater.thread.setStatus('Archived', !!+OP.archived); @@ -11206,13 +11279,8 @@ scroll = Conf['Auto Scroll'] && ThreadUpdater.scrollBG() && ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25; for (_j = 0, _len1 = posts.length; _j < _len1; _j++) { post = posts[_j]; - root = post.nodes.root; - if (post.cb) { - if (!post.cb()) { - $.add(ThreadUpdater.root, root); - } - } else { - $.add(ThreadUpdater.root, root); + if (!QuoteThreading.insert(post)) { + $.add(ThreadUpdater.root, post.nodes.root); } } $.event('PostsInserted'); @@ -11475,7 +11543,7 @@ defaultValue: 0 }); unread = quotingYou = 0; - _ref = this.response.posts.slice(1); + _ref = this.response.posts; for (_i = 0, _len = _ref.length; _i < _len; _i++) { postObj = _ref[_i]; if (!(postObj.no > lastReadPost)) { @@ -11811,68 +11879,76 @@ this.hr = $.el('hr', { id: 'unread-line' }); - this.posts = new RandomAccessList; - this.postsQuotingYou = {}; - return Thread.callbacks.push({ + this.posts = new Set; + this.postsQuotingYou = new Set; + this.order = new RandomAccessList; + this.position = null; + Thread.callbacks.push({ name: 'Unread', cb: this.node }); + return Post.callbacks.push({ + name: 'Unread', + cb: this.addPost + }); }, node: function() { + var ID, _i, _len, _ref; Unread.thread = this; Unread.title = d.title; - Unread.db.forceSync(); Unread.lastReadPost = Unread.db.get({ boardID: this.board.ID, threadID: this.ID, defaultValue: 0 }); + Unread.readCount = 0; + _ref = this.posts.keys; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + ID = _ref[_i]; + if (+ID <= Unread.lastReadPost) { + Unread.readCount++; + } + } $.one(d, '4chanXInitFinished', Unread.ready); $.on(d, 'ThreadUpdate', Unread.onUpdate); $.on(d, 'scroll visibilitychange', Unread.read); - if (Conf['Unread Line'] && !Conf['Quote Threading']) { + if (Conf['Unread Line']) { return $.on(d, 'visibilitychange', Unread.setLine); } }, ready: function() { - var posts; - if (!Conf['Quote Threading']) { - posts = []; - Unread.thread.posts.forEach(function(post) { - if (post.isReply) { - return posts.push(post); - } - }); - Unread.addPosts(posts); - } - if (Conf['Quote Threading']) { - QuoteThreading.force(); - } - if (Conf['Scroll to Last Read Post'] && !Conf['Quote Threading']) { + Unread.setLine(true); + Unread.read(); + Unread.update(); + if (Conf['Scroll to Last Read Post']) { return Unread.scroll(); } }, + positionPrev: function() { + if (Unread.position) { + return Unread.position.prev; + } else { + return Unread.order.last; + } + }, scroll: function() { - var ID, hash, posts, root, _i, _ref; + var hash, position, root; if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; } - posts = Unread.thread.posts; - _ref = posts.keys; - for (_i = _ref.length - 1; _i >= 0; _i += -1) { - ID = _ref[_i]; - if (!(+ID <= Unread.lastReadPost)) { - continue; - } - root = posts[ID].nodes.root; - if (root.getBoundingClientRect().height) { + position = Unread.positionPrev(); + while (position) { + root = position.data.nodes.root; + if (!root.getBoundingClientRect().height) { + position = position.prev; + } else { Header.scrollToIfNeeded(root, true); break; } } }, sync: function() { - var ID, lastReadPost, _i, _len, _ref; + var ID, i, lastReadPost, postIDs, _i, _ref, _ref1; if (Unread.lastReadPost == null) { return; } @@ -11885,44 +11961,36 @@ return; } Unread.lastReadPost = lastReadPost; - _ref = Unread.thread.posts.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - ID = _ref[_i]; - if (+ID > Unread.lastReadPost) { + postIDs = Unread.thread.posts.keys; + for (i = _i = _ref = Unread.readCount, _ref1 = postIDs.length; _i < _ref1; i = _i += 1) { + ID = +postIDs[i]; + if (ID > Unread.lastReadPost) { break; } - Unread.posts.rm(ID); - delete Unread.postsQuotingYou[ID]; - } - if (Conf['Unread Line'] && !Conf['Quote Threading']) { - Unread.setLine(); + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + Unread.readCount++; } + Unread.updatePosition(); + Unread.setLine(); return Unread.update(); }, - addPost: function(post) { + addPost: function() { var _ref; - if (post.ID <= Unread.lastReadPost || post.isHidden || ((_ref = QR.db) != null ? _ref.get({ - boardID: post.board.ID, - threadID: post.thread.ID, - postID: post.ID + if (this.isFetchedQuote || this.isClone) { + return; + } + Unread.order.push(this); + if (this.ID <= Unread.lastReadPost || this.isHidden || ((_ref = QR.db) != null ? _ref.get({ + boardID: this.board.ID, + threadID: this.thread.ID, + postID: this.ID }) : void 0)) { return; } - Unread.posts.push(post); - return Unread.addPostQuotingYou(post); - }, - addPosts: function(posts) { - var oldCount, post, _i, _len; - oldCount = Unread.posts.length; - for (_i = 0, _len = posts.length; _i < _len; _i++) { - post = posts[_i]; - Unread.addPost(post); - } - if (Conf['Unread Line'] && !Conf['Quote Threading']) { - Unread.setLine(oldCount === 0 && Unread.posts.length !== 0); - } - Unread.read(); - return Unread.update(); + Unread.posts.add(this.ID); + Unread.addPostQuotingYou(this); + return Unread.position != null ? Unread.position : Unread.position = Unread.order[this.ID]; }, addPostQuotingYou: function(post) { var quotelink, _i, _len, _ref, _ref1; @@ -11932,7 +12000,7 @@ if (!((_ref1 = QR.db) != null ? _ref1.get(Get.postDataFromLink(quotelink)) : void 0)) { continue; } - Unread.postsQuotingYou[post.ID] = post; + Unread.postsQuotingYou.add(post.ID); Unread.openNotification(post); return; } @@ -11957,80 +12025,76 @@ }; }, onUpdate: function(e) { - var fullID; - if (e.detail[404]) { - return Unread.update(); - } else if (!QuoteThreading.enabled) { - return Unread.addPosts((function() { - var _i, _len, _ref, _results; - _ref = e.detail.newPosts; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - fullID = _ref[_i]; - _results.push(g.posts[fullID]); - } - return _results; - })()); - } else { + if (!e.detail[404]) { + Unread.setLine(); Unread.read(); - return Unread.update(); } + return Unread.update(); }, readSinglePost: function(post) { - var ID, posts; + var ID; ID = post.ID; - posts = Unread.posts; - if (!posts[ID]) { + if (!Unread.posts.has(ID)) { return; } - posts.rm(ID); - delete Unread.postsQuotingYou[ID]; + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + Unread.updatePosition(); Unread.saveLastReadPost(); return Unread.update(); }, read: $.debounce(100, function(e) { - var ID, count, data, height, post, posts, _ref; - if (d.hidden || !Unread.posts.length) { + var ID, count, data, height, root, _ref, _ref1; + if (d.hidden || !Unread.posts.size) { return; } height = doc.clientHeight; - posts = Unread.posts; count = 0; - while (post = posts.first) { - if (!(Header.getBottomOf(post.data.nodes.root) > -1)) { + while (Unread.position) { + _ref = Unread.position, ID = _ref.ID, data = _ref.data; + root = data.nodes.root; + if (!(!root.getBoundingClientRect().height || Header.getBottomOf(root) > -1)) { break; } - ID = post.ID, data = post.data; count++; - posts.rm(ID); - delete Unread.postsQuotingYou[ID]; - if (Conf['Mark Quotes of You'] && ((_ref = QR.db) != null ? _ref.get({ + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + if (Conf['Mark Quotes of You'] && ((_ref1 = QR.db) != null ? _ref1.get({ boardID: data.board.ID, threadID: data.thread.ID, postID: ID }) : void 0)) { - QuoteYou.lastRead = data.nodes.root; + QuoteYou.lastRead = root; } + Unread.position = Unread.position.next; } if (!count) { return; } + Unread.updatePosition(); Unread.saveLastReadPost(); if (e) { return Unread.update(); } }), + updatePosition: function() { + var _results; + _results = []; + while (Unread.position && !Unread.posts.has(Unread.position.ID)) { + _results.push(Unread.position = Unread.position.next); + } + return _results; + }, saveLastReadPost: $.debounce(2 * $.SECOND, function() { - var ID, _i, _len, _ref; - _ref = Unread.thread.posts.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - ID = _ref[_i]; - if (Unread.posts[ID]) { + var ID, i, postIDs, _i, _ref, _ref1; + postIDs = Unread.thread.posts.keys; + for (i = _i = _ref = Unread.readCount, _ref1 = postIDs.length; _i < _ref1; i = _i += 1) { + ID = +postIDs[i]; + if (Unread.posts.has(ID)) { break; } - if (+ID > Unread.lastReadPost) { - Unread.lastReadPost = +ID; - } + Unread.lastReadPost = ID; + Unread.readCount++; } if (Unread.thread.isDead && !Unread.thread.isArchived) { return; @@ -12043,26 +12107,22 @@ }); }), setLine: function(force) { - var ID, posts, _i, _ref; - if (!(d.hidden || force === true)) { + if (!Conf['Unread Line']) { return; } - if (!Unread.posts.length) { - return $.rm(Unread.hr); - } - posts = Unread.thread.posts; - _ref = posts.keys; - for (_i = _ref.length - 1; _i >= 0; _i += -1) { - ID = _ref[_i]; - if (+ID <= Unread.lastReadPost) { - return $.after(posts[ID].nodes.root, Unread.hr); + if (d.hidden || (force === true)) { + if (Unread.linePosition = Unread.positionPrev()) { + $.after(Unread.linePosition.data.nodes.root, Unread.hr); + } else { + $.rm(Unread.hr); } } + return Unread.hr.hidden = Unread.linePosition === Unread.order.last; }, update: function() { var count, countQuotingYou, titleCount, titleDead, titleQuotingYou; - count = Unread.posts.length; - countQuotingYou = Object.keys(Unread.postsQuotingYou).length; + count = Unread.posts.size; + countQuotingYou = Unread.postsQuotingYou.size; if (Conf['Unread Count']) { titleQuotingYou = Conf['Quoted Title'] && countQuotingYou ? '(!) ' : ''; titleCount = count || !Conf['Hide Unread Count at (0)'] ? "(" + count + ") " : ''; @@ -13854,7 +13914,7 @@ className: 'dialog' }); $.extend(dialog, { - innerHTML: "
" + innerHTML: "
" }); $.on($('.export', Settings.dialog), 'click', Settings["export"]); $.on($('.import', Settings.dialog), 'click', Settings["import"]); @@ -14715,6 +14775,11 @@ } Main.callbackNodes(Thread, threads); return Main.callbackNodesDB(Post, posts, function() { + var post, _k, _len2; + for (_k = 0, _len2 = posts.length; _k < _len2; _k++) { + post = posts[_k]; + QuoteThreading.insert(post); + } return $.event('4chanXInitFinished'); }); } else { diff --git a/builds/4chan-X-noupdate.crx b/builds/4chan-X-noupdate.crx index 02b655a94..6544d0c1d 100644 Binary files a/builds/4chan-X-noupdate.crx and b/builds/4chan-X-noupdate.crx differ diff --git a/builds/4chan-X-noupdate.user.js b/builds/4chan-X-noupdate.user.js index 3eec22a1f..0bd6f82b2 100644 --- a/builds/4chan-X-noupdate.user.js +++ b/builds/4chan-X-noupdate.user.js @@ -1,7 +1,7 @@ // Generated by CoffeeScript // ==UserScript== // @name 4chan X -// @version 1.9.13.4 +// @version 1.9.14.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X @@ -23,7 +23,7 @@ // ==/UserScript== /* -* 4chan X - Version 1.9.13.4 +* 4chan X - Version 1.9.14.0 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE @@ -106,7 +106,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, MarkNewIPs, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, + var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, MarkNewIPs, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, ShimSet, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -387,7 +387,7 @@ doc = d.documentElement; g = { - VERSION: '1.9.13.4', + VERSION: '1.9.14.0', NAMESPACE: '4chan X.', NAME: '4chan X', FAQ: 'https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions', @@ -1886,6 +1886,40 @@ })(); + ShimSet = (function() { + function ShimSet() { + this.elements = {}; + this.size = 0; + } + + ShimSet.prototype.has = function(value) { + return value in this.elements; + }; + + ShimSet.prototype.add = function(value) { + if (this.elements[value]) { + return; + } + this.elements[value] = true; + return this.size++; + }; + + ShimSet.prototype["delete"] = function(value) { + if (!this.elements[value]) { + return; + } + delete this.elements[value]; + return this.size--; + }; + + return ShimSet; + + })(); + + if (!('Set' in window)) { + window.Set = ShimSet; + } + Polyfill = { init: function() { this.notificationPermission(); @@ -6023,7 +6057,6 @@ QuoteThreading = { init: function() { - var input; if (!(Conf['Quote Threading'] && g.VIEW === 'thread')) { return; } @@ -6031,123 +6064,164 @@ this.controls = $.el('span', { innerHTML: "" }); - input = $('input', this.controls); - $.on(input, 'change', this.toggle); + this.threadNewLink = $.el('span', { + className: 'brackets-wrap threadnewlink', + hidden: true + }); + $.extend(this.threadNewLink, { + innerHTML: "Thread New Posts" + }); + $.on($('input', this.controls), 'change', function() { + return QuoteThreading.rethread(this.checked); + }); + $.on(this.threadNewLink.firstElementChild, 'click', function() { + QuoteThreading.threadNewLink.hidden = true; + return QuoteThreading.rethread(true); + }); Header.menu.addEntry(this.entry = { el: this.controls, order: 98 }); + Thread.callbacks.push({ + name: 'Quote Threading', + cb: this.setThread + }); return Post.callbacks.push({ name: 'Quote Threading', cb: this.node }); }, - force: function() { - g.posts.forEach(function(post) { - if (post.cb) { - return post.cb(true); - } + parent: {}, + children: {}, + inserted: {}, + setThread: function() { + QuoteThreading.thread = this; + return $.asap((function() { + return !Conf['Thread Updater'] || $('.navLinksBot > .updatelink'); + }), function() { + return $.add($('.navLinksBot'), [$.tn(' '), QuoteThreading.threadNewLink]); }); - Unread.read(); - return Unread.update(); }, node: function() { - var keys, len, posts, quote, _i, _len, _ref; - posts = g.posts; - if (this.isClone || !QuoteThreading.enabled) { + var parent, parents, quote, thread; + if (this.isFetchedQuote || this.isClone || !this.isReply) { return; } - Unread.addPost(this); - if (this.thread.OP === this || this.isHidden) { - return; + thread = QuoteThreading.thread; + parents = (function() { + var _i, _len, _ref, _results; + _ref = this.quotes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + quote = _ref[_i]; + parent = g.posts[quote]; + if (!parent || parent.isFetchedQuote || !parent.isReply || parent.ID >= this.ID) { + continue; + } + _results.push(parent); + } + return _results; + }).call(this); + if (parents.length === 1) { + return QuoteThreading.parent[this.fullID] = parents[0]; } - keys = []; - len = g.BOARD.ID.length + 1; - _ref = this.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if ((quote.slice(len) < this.ID) && quote in posts) { - keys.push(quote); + }, + descendants: function(post) { + var child, children, posts, _i, _len; + posts = [post]; + if (children = QuoteThreading.children[post.fullID]) { + for (_i = 0, _len = children.length; _i < _len; _i++) { + child = children[_i]; + posts = posts.concat(QuoteThreading.descendants(child)); } } - if (keys.length !== 1) { - return; - } - this.threaded = keys[0]; - return this.cb = QuoteThreading.nodeinsert; + return posts; }, - nodeinsert: function(force) { - var bottom, height, post, posts, root, threadContainer, top, _ref; - post = g.posts[this.threaded]; - if (this.thread.OP === post) { + insert: function(post) { + var child, children, descendants, i, next, nodes, order, parent, prev, prev2, threadContainer, x, _base, _i, _j, _k, _len, _name; + if (!(QuoteThreading.enabled && (parent = QuoteThreading.parent[post.fullID]) && !QuoteThreading.inserted[post.fullID])) { return false; } - posts = Unread.posts; - root = post.nodes.root; - if (!force) { - height = doc.clientHeight; - _ref = root.getBoundingClientRect(), bottom = _ref.bottom, top = _ref.top; - if (!(posts[post.ID] || ((bottom < height) && (top > 0)))) { - return false; + descendants = QuoteThreading.descendants(post); + if (!Unread.posts.has(parent.ID) && descendants.some(function(x) { + return Unread.posts.has(x.ID); + })) { + QuoteThreading.threadNewLink.hidden = false; + return false; + } + order = Unread.order; + children = ((_base = QuoteThreading.children)[_name = parent.fullID] || (_base[_name] = [])); + threadContainer = parent.nodes.threadContainer || $.el('div', { + className: 'threadContainer' + }); + nodes = [post.nodes.root]; + if (post.nodes.threadContainer) { + nodes.push(post.nodes.threadContainer); + } + i = children.length; + for (_i = children.length - 1; _i >= 0; _i += -1) { + child = children[_i]; + if (child.ID >= post.ID) { + i--; } } - if ($.hasClass(root, 'threadOP')) { - threadContainer = root.nextElementSibling; - post = Get.postFromRoot($.x('descendant::div[contains(@class,"postContainer")][last()]', threadContainer)); - $.add(threadContainer, this.nodes.root); + if (i !== children.length) { + next = children[i]; + for (_j = 0, _len = descendants.length; _j < _len; _j++) { + x = descendants[_j]; + order.before(order[next.ID], order[x.ID]); + } + children.splice(i, 0, post); + $.before(next.nodes.root, nodes); } else { - threadContainer = $.el('div', { - className: 'threadContainer' - }); - $.add(threadContainer, this.nodes.root); - $.after(root, threadContainer); - $.addClass(root, 'threadOP'); + prev = parent; + while ((prev2 = QuoteThreading.children[prev.fullID]) && prev2.length) { + prev = prev2[prev2.length - 1]; + } + for (_k = descendants.length - 1; _k >= 0; _k += -1) { + x = descendants[_k]; + order.after(order[prev.ID], order[x.ID]); + } + children.push(post); + $.add(threadContainer, nodes); } - if (post = posts[post.ID]) { - posts.after(post, posts[this.ID]); - } else if (posts[this.ID]) { - posts.prepend(posts[this.ID]); + QuoteThreading.inserted[post.fullID] = true; + if (!parent.nodes.threadContainer) { + parent.nodes.threadContainer = threadContainer; + $.addClass(parent.nodes.root, 'threadOP'); + $.after(parent.nodes.root, threadContainer); } return true; }, - toggle: function() { - var container, containers, nodes, post, posts, thread, _i, _j, _k, _len, _len1, _len2, _ref; - if (QuoteThreading.enabled = this.checked) { - QuoteThreading.force(); + rethread: function(enabled) { + var nodes, posts, thread; + thread = QuoteThreading.thread; + posts = thread.posts; + if (QuoteThreading.enabled = enabled) { + posts.forEach(QuoteThreading.insert); } else { - thread = $('.thread'); - posts = []; nodes = []; - g.posts.forEach(function(post) { - if (!(post === post.thread.OP || post.isClone)) { - return posts.push(post); + Unread.order = new RandomAccessList; + QuoteThreading.inserted = {}; + posts.forEach(function(post) { + Unread.order.push(post); + if (post.isReply) { + nodes.push(post.nodes.root); + } + if (QuoteThreading.children[post.fullID]) { + delete QuoteThreading.children[post.fullID]; + $.rmClass(post.nodes.root, 'threadOP'); + $.rm(post.nodes.threadContainer); + return delete post.nodes.threadContainer; } }); - posts.sort(function(a, b) { - return a.ID - b.ID; - }); - for (_i = 0, _len = posts.length; _i < _len; _i++) { - post = posts[_i]; - nodes.push(post.nodes.root); - } - $.add(thread, nodes); - containers = $$('.threadContainer', thread); - for (_j = 0, _len1 = containers.length; _j < _len1; _j++) { - container = containers[_j]; - $.rm(container); - } - _ref = $$('.threadOP'); - for (_k = 0, _len2 = _ref.length; _k < _len2; _k++) { - post = _ref[_k]; - $.rmClass(post, 'threadOP'); - } + $.add(thread.OP.nodes.root.parentNode, nodes); } - }, - kb: function() { - var control; - control = $.id('threadingControl'); - control.checked = !control.checked; - return QuoteThreading.toggle.call(control); + Unread.position = Unread.order.first; + Unread.updatePosition(); + Unread.setLine(true); + Unread.read(); + return Unread.update(); } }; @@ -8861,44 +8935,34 @@ } }, toggle: function(post) { - var headRect, left, root, top, x, y, _ref; + var next; if (!(post.file.isExpanding || post.file.isExpanded)) { post.file.scrollIntoView = Conf['Scroll into view']; ImageExpand.expand(post); return; } ImageExpand.contract(post); - root = post.nodes.root; - _ref = (Conf['Advance on contract'] ? (function() { - var next; - next = root; + if (Conf['Advance on contract']) { + next = post.nodes.root; while (next = $.x("following::div[contains(@class,'postContainer')][1]", next)) { - if ($('.stub', next) || next.offsetHeight === 0) { - continue; + if (!($('.stub', next) || next.offsetHeight === 0)) { + break; } - return next; } - return root; - })() : root).getBoundingClientRect(), top = _ref.top, left = _ref.left; - if (top < 0) { - y = top; - if (Conf['Fixed Header'] && !Conf['Bottom Header']) { - headRect = Header.bar.getBoundingClientRect(); - y -= headRect.top + headRect.height; + if (next) { + return Header.scrollTo(next); } } - if (left < 0) { - x = -window.scrollX; - } - if (x || y) { - return window.scrollBy(x, y); - } }, contract: function(post) { - var bottom, cb, el, eventName, file, oldHeight, x, _i, _len, _ref, _ref1; + var bottom, cb, el, eventName, file, oldHeight, scrollY, top, x, _i, _len, _ref, _ref1; file = post.file; - bottom = post.nodes.root.getBoundingClientRect().bottom; - oldHeight = d.body.clientHeight; + if (el = file.fullImage) { + top = Header.getTopOf(el); + bottom = top + el.getBoundingClientRect().height; + oldHeight = d.body.clientHeight; + scrollY = window.scrollY; + } $.rmClass(post.nodes.root, 'expanded-image'); $.rmClass(file.thumb, 'expanding'); if (file.videoControls) { @@ -8911,35 +8975,43 @@ x = _ref[_i]; delete file[x]; } - if (doc.contains(post.nodes.root) && bottom <= 0) { - window.scrollBy(0, d.body.clientHeight - oldHeight); + if (!el) { + return; } - if (el = file.fullImage) { - $.off(el, 'error', ImageExpand.error); - ImageCommon.pushCache(el); - if (file.isVideo) { - el.pause(); - _ref1 = ImageExpand.videoCB; - for (eventName in _ref1) { - cb = _ref1[eventName]; - $.off(el, eventName, cb); - } + if (doc.contains(el)) { + if (bottom <= 0) { + window.scroll(0, scrollY + d.body.clientHeight - oldHeight); + } else { + Header.scrollToIfNeeded(post.nodes.root); } - if (Conf['Restart when Opened']) { - ImageCommon.rewind(file.thumb); + if (window.scrollX > 0) { + window.scroll(0, window.scrollY); } - delete file.fullImage; - return $.queueTask(function() { - if (file.isExpanding || file.isExpanded) { - return; - } - $.rmClass(el, 'full-image'); - if (el.id) { - return; - } - return $.rm(el); - }); } + $.off(el, 'error', ImageExpand.error); + ImageCommon.pushCache(el); + if (file.isVideo) { + el.pause(); + _ref1 = ImageExpand.videoCB; + for (eventName in _ref1) { + cb = _ref1[eventName]; + $.off(el, eventName, cb); + } + } + if (Conf['Restart when Opened']) { + ImageCommon.rewind(file.thumb); + } + delete file.fullImage; + return $.queueTask(function() { + if (file.isExpanding || file.isExpanded) { + return; + } + $.rmClass(el, 'full-image'); + if (el.id) { + return; + } + return $.rm(el); + }); }, expand: function(post, src) { var el, file, isVideo, thumb, _ref; @@ -8992,19 +9064,20 @@ } }, completeExpand: function(post) { - var bottom, file, imageBottom, oldHeight; + var bottom, file, imageBottom, oldHeight, scrollY; file = post.file; if (!file.isExpanding) { return; } - bottom = post.nodes.root.getBoundingClientRect().bottom; + bottom = Header.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; oldHeight = d.body.clientHeight; + scrollY = window.scrollY; $.addClass(post.nodes.root, 'expanded-image'); $.rmClass(file.thumb, 'expanding'); file.isExpanded = true; delete file.isExpanding; if (doc.contains(post.nodes.root) && bottom <= 0) { - window.scrollBy(0, d.body.clientHeight - oldHeight); + window.scroll(window.scrollX, scrollY + d.body.clientHeight - oldHeight); } if (file.scrollIntoView) { delete file.scrollIntoView; @@ -9206,7 +9279,7 @@ ImageCommon.rewind(this); } el.id = 'ihover'; - $.after(Header.hover, el); + $.add(Header.hover, el); if (isVideo) { el.loop = true; el.controls = false; @@ -11142,7 +11215,7 @@ return new Notice('info', "The thread is " + change + ".", 30); }, parse: function(postObjects) { - var OP, count, files, index, ipCountEl, node, num, post, postObject, posts, root, scroll, _i, _j, _len, _len1; + var OP, count, files, index, ipCountEl, node, num, post, postObject, posts, scroll, _i, _j, _len, _len1; OP = postObjects[0]; Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler; ThreadUpdater.thread.setStatus('Archived', !!+OP.archived); @@ -11205,13 +11278,8 @@ scroll = Conf['Auto Scroll'] && ThreadUpdater.scrollBG() && ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25; for (_j = 0, _len1 = posts.length; _j < _len1; _j++) { post = posts[_j]; - root = post.nodes.root; - if (post.cb) { - if (!post.cb()) { - $.add(ThreadUpdater.root, root); - } - } else { - $.add(ThreadUpdater.root, root); + if (!QuoteThreading.insert(post)) { + $.add(ThreadUpdater.root, post.nodes.root); } } $.event('PostsInserted'); @@ -11474,7 +11542,7 @@ defaultValue: 0 }); unread = quotingYou = 0; - _ref = this.response.posts.slice(1); + _ref = this.response.posts; for (_i = 0, _len = _ref.length; _i < _len; _i++) { postObj = _ref[_i]; if (!(postObj.no > lastReadPost)) { @@ -11810,68 +11878,76 @@ this.hr = $.el('hr', { id: 'unread-line' }); - this.posts = new RandomAccessList; - this.postsQuotingYou = {}; - return Thread.callbacks.push({ + this.posts = new Set; + this.postsQuotingYou = new Set; + this.order = new RandomAccessList; + this.position = null; + Thread.callbacks.push({ name: 'Unread', cb: this.node }); + return Post.callbacks.push({ + name: 'Unread', + cb: this.addPost + }); }, node: function() { + var ID, _i, _len, _ref; Unread.thread = this; Unread.title = d.title; - Unread.db.forceSync(); Unread.lastReadPost = Unread.db.get({ boardID: this.board.ID, threadID: this.ID, defaultValue: 0 }); + Unread.readCount = 0; + _ref = this.posts.keys; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + ID = _ref[_i]; + if (+ID <= Unread.lastReadPost) { + Unread.readCount++; + } + } $.one(d, '4chanXInitFinished', Unread.ready); $.on(d, 'ThreadUpdate', Unread.onUpdate); $.on(d, 'scroll visibilitychange', Unread.read); - if (Conf['Unread Line'] && !Conf['Quote Threading']) { + if (Conf['Unread Line']) { return $.on(d, 'visibilitychange', Unread.setLine); } }, ready: function() { - var posts; - if (!Conf['Quote Threading']) { - posts = []; - Unread.thread.posts.forEach(function(post) { - if (post.isReply) { - return posts.push(post); - } - }); - Unread.addPosts(posts); - } - if (Conf['Quote Threading']) { - QuoteThreading.force(); - } - if (Conf['Scroll to Last Read Post'] && !Conf['Quote Threading']) { + Unread.setLine(true); + Unread.read(); + Unread.update(); + if (Conf['Scroll to Last Read Post']) { return Unread.scroll(); } }, + positionPrev: function() { + if (Unread.position) { + return Unread.position.prev; + } else { + return Unread.order.last; + } + }, scroll: function() { - var ID, hash, posts, root, _i, _ref; + var hash, position, root; if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; } - posts = Unread.thread.posts; - _ref = posts.keys; - for (_i = _ref.length - 1; _i >= 0; _i += -1) { - ID = _ref[_i]; - if (!(+ID <= Unread.lastReadPost)) { - continue; - } - root = posts[ID].nodes.root; - if (root.getBoundingClientRect().height) { + position = Unread.positionPrev(); + while (position) { + root = position.data.nodes.root; + if (!root.getBoundingClientRect().height) { + position = position.prev; + } else { Header.scrollToIfNeeded(root, true); break; } } }, sync: function() { - var ID, lastReadPost, _i, _len, _ref; + var ID, i, lastReadPost, postIDs, _i, _ref, _ref1; if (Unread.lastReadPost == null) { return; } @@ -11884,44 +11960,36 @@ return; } Unread.lastReadPost = lastReadPost; - _ref = Unread.thread.posts.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - ID = _ref[_i]; - if (+ID > Unread.lastReadPost) { + postIDs = Unread.thread.posts.keys; + for (i = _i = _ref = Unread.readCount, _ref1 = postIDs.length; _i < _ref1; i = _i += 1) { + ID = +postIDs[i]; + if (ID > Unread.lastReadPost) { break; } - Unread.posts.rm(ID); - delete Unread.postsQuotingYou[ID]; - } - if (Conf['Unread Line'] && !Conf['Quote Threading']) { - Unread.setLine(); + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + Unread.readCount++; } + Unread.updatePosition(); + Unread.setLine(); return Unread.update(); }, - addPost: function(post) { + addPost: function() { var _ref; - if (post.ID <= Unread.lastReadPost || post.isHidden || ((_ref = QR.db) != null ? _ref.get({ - boardID: post.board.ID, - threadID: post.thread.ID, - postID: post.ID + if (this.isFetchedQuote || this.isClone) { + return; + } + Unread.order.push(this); + if (this.ID <= Unread.lastReadPost || this.isHidden || ((_ref = QR.db) != null ? _ref.get({ + boardID: this.board.ID, + threadID: this.thread.ID, + postID: this.ID }) : void 0)) { return; } - Unread.posts.push(post); - return Unread.addPostQuotingYou(post); - }, - addPosts: function(posts) { - var oldCount, post, _i, _len; - oldCount = Unread.posts.length; - for (_i = 0, _len = posts.length; _i < _len; _i++) { - post = posts[_i]; - Unread.addPost(post); - } - if (Conf['Unread Line'] && !Conf['Quote Threading']) { - Unread.setLine(oldCount === 0 && Unread.posts.length !== 0); - } - Unread.read(); - return Unread.update(); + Unread.posts.add(this.ID); + Unread.addPostQuotingYou(this); + return Unread.position != null ? Unread.position : Unread.position = Unread.order[this.ID]; }, addPostQuotingYou: function(post) { var quotelink, _i, _len, _ref, _ref1; @@ -11931,7 +11999,7 @@ if (!((_ref1 = QR.db) != null ? _ref1.get(Get.postDataFromLink(quotelink)) : void 0)) { continue; } - Unread.postsQuotingYou[post.ID] = post; + Unread.postsQuotingYou.add(post.ID); Unread.openNotification(post); return; } @@ -11956,80 +12024,76 @@ }; }, onUpdate: function(e) { - var fullID; - if (e.detail[404]) { - return Unread.update(); - } else if (!QuoteThreading.enabled) { - return Unread.addPosts((function() { - var _i, _len, _ref, _results; - _ref = e.detail.newPosts; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - fullID = _ref[_i]; - _results.push(g.posts[fullID]); - } - return _results; - })()); - } else { + if (!e.detail[404]) { + Unread.setLine(); Unread.read(); - return Unread.update(); } + return Unread.update(); }, readSinglePost: function(post) { - var ID, posts; + var ID; ID = post.ID; - posts = Unread.posts; - if (!posts[ID]) { + if (!Unread.posts.has(ID)) { return; } - posts.rm(ID); - delete Unread.postsQuotingYou[ID]; + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + Unread.updatePosition(); Unread.saveLastReadPost(); return Unread.update(); }, read: $.debounce(100, function(e) { - var ID, count, data, height, post, posts, _ref; - if (d.hidden || !Unread.posts.length) { + var ID, count, data, height, root, _ref, _ref1; + if (d.hidden || !Unread.posts.size) { return; } height = doc.clientHeight; - posts = Unread.posts; count = 0; - while (post = posts.first) { - if (!(Header.getBottomOf(post.data.nodes.root) > -1)) { + while (Unread.position) { + _ref = Unread.position, ID = _ref.ID, data = _ref.data; + root = data.nodes.root; + if (!(!root.getBoundingClientRect().height || Header.getBottomOf(root) > -1)) { break; } - ID = post.ID, data = post.data; count++; - posts.rm(ID); - delete Unread.postsQuotingYou[ID]; - if (Conf['Mark Quotes of You'] && ((_ref = QR.db) != null ? _ref.get({ + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + if (Conf['Mark Quotes of You'] && ((_ref1 = QR.db) != null ? _ref1.get({ boardID: data.board.ID, threadID: data.thread.ID, postID: ID }) : void 0)) { - QuoteYou.lastRead = data.nodes.root; + QuoteYou.lastRead = root; } + Unread.position = Unread.position.next; } if (!count) { return; } + Unread.updatePosition(); Unread.saveLastReadPost(); if (e) { return Unread.update(); } }), + updatePosition: function() { + var _results; + _results = []; + while (Unread.position && !Unread.posts.has(Unread.position.ID)) { + _results.push(Unread.position = Unread.position.next); + } + return _results; + }, saveLastReadPost: $.debounce(2 * $.SECOND, function() { - var ID, _i, _len, _ref; - _ref = Unread.thread.posts.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - ID = _ref[_i]; - if (Unread.posts[ID]) { + var ID, i, postIDs, _i, _ref, _ref1; + postIDs = Unread.thread.posts.keys; + for (i = _i = _ref = Unread.readCount, _ref1 = postIDs.length; _i < _ref1; i = _i += 1) { + ID = +postIDs[i]; + if (Unread.posts.has(ID)) { break; } - if (+ID > Unread.lastReadPost) { - Unread.lastReadPost = +ID; - } + Unread.lastReadPost = ID; + Unread.readCount++; } if (Unread.thread.isDead && !Unread.thread.isArchived) { return; @@ -12042,26 +12106,22 @@ }); }), setLine: function(force) { - var ID, posts, _i, _ref; - if (!(d.hidden || force === true)) { + if (!Conf['Unread Line']) { return; } - if (!Unread.posts.length) { - return $.rm(Unread.hr); - } - posts = Unread.thread.posts; - _ref = posts.keys; - for (_i = _ref.length - 1; _i >= 0; _i += -1) { - ID = _ref[_i]; - if (+ID <= Unread.lastReadPost) { - return $.after(posts[ID].nodes.root, Unread.hr); + if (d.hidden || (force === true)) { + if (Unread.linePosition = Unread.positionPrev()) { + $.after(Unread.linePosition.data.nodes.root, Unread.hr); + } else { + $.rm(Unread.hr); } } + return Unread.hr.hidden = Unread.linePosition === Unread.order.last; }, update: function() { var count, countQuotingYou, titleCount, titleDead, titleQuotingYou; - count = Unread.posts.length; - countQuotingYou = Object.keys(Unread.postsQuotingYou).length; + count = Unread.posts.size; + countQuotingYou = Unread.postsQuotingYou.size; if (Conf['Unread Count']) { titleQuotingYou = Conf['Quoted Title'] && countQuotingYou ? '(!) ' : ''; titleCount = count || !Conf['Hide Unread Count at (0)'] ? "(" + count + ") " : ''; @@ -13853,7 +13913,7 @@ className: 'dialog' }); $.extend(dialog, { - innerHTML: "
" + innerHTML: "
" }); $.on($('.export', Settings.dialog), 'click', Settings["export"]); $.on($('.import', Settings.dialog), 'click', Settings["import"]); @@ -14714,6 +14774,11 @@ } Main.callbackNodes(Thread, threads); return Main.callbackNodesDB(Post, posts, function() { + var post, _k, _len2; + for (_k = 0, _len2 = posts.length; _k < _len2; _k++) { + post = posts[_k]; + QuoteThreading.insert(post); + } return $.event('4chanXInitFinished'); }); } else { diff --git a/builds/4chan-X.crx b/builds/4chan-X.crx index 91085cf62..6b0172a06 100644 Binary files a/builds/4chan-X.crx and b/builds/4chan-X.crx differ diff --git a/builds/4chan-X.meta.js b/builds/4chan-X.meta.js index ca1140754..d6ce6e4ef 100644 --- a/builds/4chan-X.meta.js +++ b/builds/4chan-X.meta.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X -// @version 1.9.13.4 +// @version 1.9.14.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js index 39240e837..d7ea401ad 100644 --- a/builds/4chan-X.user.js +++ b/builds/4chan-X.user.js @@ -1,7 +1,7 @@ // Generated by CoffeeScript // ==UserScript== // @name 4chan X -// @version 1.9.13.4 +// @version 1.9.14.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X @@ -24,7 +24,7 @@ // ==/UserScript== /* -* 4chan X - Version 1.9.13.4 +* 4chan X - Version 1.9.14.0 * * Licensed under the MIT license. * https://github.com/ccd0/4chan-x/blob/master/LICENSE @@ -107,7 +107,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, MarkNewIPs, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, + var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, Keybinds, Linkify, Main, MarkNewIPs, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Sauce, Settings, ShimSet, SimpleDict, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -388,7 +388,7 @@ doc = d.documentElement; g = { - VERSION: '1.9.13.4', + VERSION: '1.9.14.0', NAMESPACE: '4chan X.', NAME: '4chan X', FAQ: 'https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions', @@ -1887,6 +1887,40 @@ })(); + ShimSet = (function() { + function ShimSet() { + this.elements = {}; + this.size = 0; + } + + ShimSet.prototype.has = function(value) { + return value in this.elements; + }; + + ShimSet.prototype.add = function(value) { + if (this.elements[value]) { + return; + } + this.elements[value] = true; + return this.size++; + }; + + ShimSet.prototype["delete"] = function(value) { + if (!this.elements[value]) { + return; + } + delete this.elements[value]; + return this.size--; + }; + + return ShimSet; + + })(); + + if (!('Set' in window)) { + window.Set = ShimSet; + } + Polyfill = { init: function() { this.notificationPermission(); @@ -6024,7 +6058,6 @@ QuoteThreading = { init: function() { - var input; if (!(Conf['Quote Threading'] && g.VIEW === 'thread')) { return; } @@ -6032,123 +6065,164 @@ this.controls = $.el('span', { innerHTML: "" }); - input = $('input', this.controls); - $.on(input, 'change', this.toggle); + this.threadNewLink = $.el('span', { + className: 'brackets-wrap threadnewlink', + hidden: true + }); + $.extend(this.threadNewLink, { + innerHTML: "Thread New Posts" + }); + $.on($('input', this.controls), 'change', function() { + return QuoteThreading.rethread(this.checked); + }); + $.on(this.threadNewLink.firstElementChild, 'click', function() { + QuoteThreading.threadNewLink.hidden = true; + return QuoteThreading.rethread(true); + }); Header.menu.addEntry(this.entry = { el: this.controls, order: 98 }); + Thread.callbacks.push({ + name: 'Quote Threading', + cb: this.setThread + }); return Post.callbacks.push({ name: 'Quote Threading', cb: this.node }); }, - force: function() { - g.posts.forEach(function(post) { - if (post.cb) { - return post.cb(true); - } + parent: {}, + children: {}, + inserted: {}, + setThread: function() { + QuoteThreading.thread = this; + return $.asap((function() { + return !Conf['Thread Updater'] || $('.navLinksBot > .updatelink'); + }), function() { + return $.add($('.navLinksBot'), [$.tn(' '), QuoteThreading.threadNewLink]); }); - Unread.read(); - return Unread.update(); }, node: function() { - var keys, len, posts, quote, _i, _len, _ref; - posts = g.posts; - if (this.isClone || !QuoteThreading.enabled) { + var parent, parents, quote, thread; + if (this.isFetchedQuote || this.isClone || !this.isReply) { return; } - Unread.addPost(this); - if (this.thread.OP === this || this.isHidden) { - return; + thread = QuoteThreading.thread; + parents = (function() { + var _i, _len, _ref, _results; + _ref = this.quotes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + quote = _ref[_i]; + parent = g.posts[quote]; + if (!parent || parent.isFetchedQuote || !parent.isReply || parent.ID >= this.ID) { + continue; + } + _results.push(parent); + } + return _results; + }).call(this); + if (parents.length === 1) { + return QuoteThreading.parent[this.fullID] = parents[0]; } - keys = []; - len = g.BOARD.ID.length + 1; - _ref = this.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if ((quote.slice(len) < this.ID) && quote in posts) { - keys.push(quote); + }, + descendants: function(post) { + var child, children, posts, _i, _len; + posts = [post]; + if (children = QuoteThreading.children[post.fullID]) { + for (_i = 0, _len = children.length; _i < _len; _i++) { + child = children[_i]; + posts = posts.concat(QuoteThreading.descendants(child)); } } - if (keys.length !== 1) { - return; - } - this.threaded = keys[0]; - return this.cb = QuoteThreading.nodeinsert; + return posts; }, - nodeinsert: function(force) { - var bottom, height, post, posts, root, threadContainer, top, _ref; - post = g.posts[this.threaded]; - if (this.thread.OP === post) { + insert: function(post) { + var child, children, descendants, i, next, nodes, order, parent, prev, prev2, threadContainer, x, _base, _i, _j, _k, _len, _name; + if (!(QuoteThreading.enabled && (parent = QuoteThreading.parent[post.fullID]) && !QuoteThreading.inserted[post.fullID])) { return false; } - posts = Unread.posts; - root = post.nodes.root; - if (!force) { - height = doc.clientHeight; - _ref = root.getBoundingClientRect(), bottom = _ref.bottom, top = _ref.top; - if (!(posts[post.ID] || ((bottom < height) && (top > 0)))) { - return false; + descendants = QuoteThreading.descendants(post); + if (!Unread.posts.has(parent.ID) && descendants.some(function(x) { + return Unread.posts.has(x.ID); + })) { + QuoteThreading.threadNewLink.hidden = false; + return false; + } + order = Unread.order; + children = ((_base = QuoteThreading.children)[_name = parent.fullID] || (_base[_name] = [])); + threadContainer = parent.nodes.threadContainer || $.el('div', { + className: 'threadContainer' + }); + nodes = [post.nodes.root]; + if (post.nodes.threadContainer) { + nodes.push(post.nodes.threadContainer); + } + i = children.length; + for (_i = children.length - 1; _i >= 0; _i += -1) { + child = children[_i]; + if (child.ID >= post.ID) { + i--; } } - if ($.hasClass(root, 'threadOP')) { - threadContainer = root.nextElementSibling; - post = Get.postFromRoot($.x('descendant::div[contains(@class,"postContainer")][last()]', threadContainer)); - $.add(threadContainer, this.nodes.root); + if (i !== children.length) { + next = children[i]; + for (_j = 0, _len = descendants.length; _j < _len; _j++) { + x = descendants[_j]; + order.before(order[next.ID], order[x.ID]); + } + children.splice(i, 0, post); + $.before(next.nodes.root, nodes); } else { - threadContainer = $.el('div', { - className: 'threadContainer' - }); - $.add(threadContainer, this.nodes.root); - $.after(root, threadContainer); - $.addClass(root, 'threadOP'); + prev = parent; + while ((prev2 = QuoteThreading.children[prev.fullID]) && prev2.length) { + prev = prev2[prev2.length - 1]; + } + for (_k = descendants.length - 1; _k >= 0; _k += -1) { + x = descendants[_k]; + order.after(order[prev.ID], order[x.ID]); + } + children.push(post); + $.add(threadContainer, nodes); } - if (post = posts[post.ID]) { - posts.after(post, posts[this.ID]); - } else if (posts[this.ID]) { - posts.prepend(posts[this.ID]); + QuoteThreading.inserted[post.fullID] = true; + if (!parent.nodes.threadContainer) { + parent.nodes.threadContainer = threadContainer; + $.addClass(parent.nodes.root, 'threadOP'); + $.after(parent.nodes.root, threadContainer); } return true; }, - toggle: function() { - var container, containers, nodes, post, posts, thread, _i, _j, _k, _len, _len1, _len2, _ref; - if (QuoteThreading.enabled = this.checked) { - QuoteThreading.force(); + rethread: function(enabled) { + var nodes, posts, thread; + thread = QuoteThreading.thread; + posts = thread.posts; + if (QuoteThreading.enabled = enabled) { + posts.forEach(QuoteThreading.insert); } else { - thread = $('.thread'); - posts = []; nodes = []; - g.posts.forEach(function(post) { - if (!(post === post.thread.OP || post.isClone)) { - return posts.push(post); + Unread.order = new RandomAccessList; + QuoteThreading.inserted = {}; + posts.forEach(function(post) { + Unread.order.push(post); + if (post.isReply) { + nodes.push(post.nodes.root); + } + if (QuoteThreading.children[post.fullID]) { + delete QuoteThreading.children[post.fullID]; + $.rmClass(post.nodes.root, 'threadOP'); + $.rm(post.nodes.threadContainer); + return delete post.nodes.threadContainer; } }); - posts.sort(function(a, b) { - return a.ID - b.ID; - }); - for (_i = 0, _len = posts.length; _i < _len; _i++) { - post = posts[_i]; - nodes.push(post.nodes.root); - } - $.add(thread, nodes); - containers = $$('.threadContainer', thread); - for (_j = 0, _len1 = containers.length; _j < _len1; _j++) { - container = containers[_j]; - $.rm(container); - } - _ref = $$('.threadOP'); - for (_k = 0, _len2 = _ref.length; _k < _len2; _k++) { - post = _ref[_k]; - $.rmClass(post, 'threadOP'); - } + $.add(thread.OP.nodes.root.parentNode, nodes); } - }, - kb: function() { - var control; - control = $.id('threadingControl'); - control.checked = !control.checked; - return QuoteThreading.toggle.call(control); + Unread.position = Unread.order.first; + Unread.updatePosition(); + Unread.setLine(true); + Unread.read(); + return Unread.update(); } }; @@ -8862,44 +8936,34 @@ } }, toggle: function(post) { - var headRect, left, root, top, x, y, _ref; + var next; if (!(post.file.isExpanding || post.file.isExpanded)) { post.file.scrollIntoView = Conf['Scroll into view']; ImageExpand.expand(post); return; } ImageExpand.contract(post); - root = post.nodes.root; - _ref = (Conf['Advance on contract'] ? (function() { - var next; - next = root; + if (Conf['Advance on contract']) { + next = post.nodes.root; while (next = $.x("following::div[contains(@class,'postContainer')][1]", next)) { - if ($('.stub', next) || next.offsetHeight === 0) { - continue; + if (!($('.stub', next) || next.offsetHeight === 0)) { + break; } - return next; } - return root; - })() : root).getBoundingClientRect(), top = _ref.top, left = _ref.left; - if (top < 0) { - y = top; - if (Conf['Fixed Header'] && !Conf['Bottom Header']) { - headRect = Header.bar.getBoundingClientRect(); - y -= headRect.top + headRect.height; + if (next) { + return Header.scrollTo(next); } } - if (left < 0) { - x = -window.scrollX; - } - if (x || y) { - return window.scrollBy(x, y); - } }, contract: function(post) { - var bottom, cb, el, eventName, file, oldHeight, x, _i, _len, _ref, _ref1; + var bottom, cb, el, eventName, file, oldHeight, scrollY, top, x, _i, _len, _ref, _ref1; file = post.file; - bottom = post.nodes.root.getBoundingClientRect().bottom; - oldHeight = d.body.clientHeight; + if (el = file.fullImage) { + top = Header.getTopOf(el); + bottom = top + el.getBoundingClientRect().height; + oldHeight = d.body.clientHeight; + scrollY = window.scrollY; + } $.rmClass(post.nodes.root, 'expanded-image'); $.rmClass(file.thumb, 'expanding'); if (file.videoControls) { @@ -8912,35 +8976,43 @@ x = _ref[_i]; delete file[x]; } - if (doc.contains(post.nodes.root) && bottom <= 0) { - window.scrollBy(0, d.body.clientHeight - oldHeight); + if (!el) { + return; } - if (el = file.fullImage) { - $.off(el, 'error', ImageExpand.error); - ImageCommon.pushCache(el); - if (file.isVideo) { - el.pause(); - _ref1 = ImageExpand.videoCB; - for (eventName in _ref1) { - cb = _ref1[eventName]; - $.off(el, eventName, cb); - } + if (doc.contains(el)) { + if (bottom <= 0) { + window.scroll(0, scrollY + d.body.clientHeight - oldHeight); + } else { + Header.scrollToIfNeeded(post.nodes.root); } - if (Conf['Restart when Opened']) { - ImageCommon.rewind(file.thumb); + if (window.scrollX > 0) { + window.scroll(0, window.scrollY); } - delete file.fullImage; - return $.queueTask(function() { - if (file.isExpanding || file.isExpanded) { - return; - } - $.rmClass(el, 'full-image'); - if (el.id) { - return; - } - return $.rm(el); - }); } + $.off(el, 'error', ImageExpand.error); + ImageCommon.pushCache(el); + if (file.isVideo) { + el.pause(); + _ref1 = ImageExpand.videoCB; + for (eventName in _ref1) { + cb = _ref1[eventName]; + $.off(el, eventName, cb); + } + } + if (Conf['Restart when Opened']) { + ImageCommon.rewind(file.thumb); + } + delete file.fullImage; + return $.queueTask(function() { + if (file.isExpanding || file.isExpanded) { + return; + } + $.rmClass(el, 'full-image'); + if (el.id) { + return; + } + return $.rm(el); + }); }, expand: function(post, src) { var el, file, isVideo, thumb, _ref; @@ -8993,19 +9065,20 @@ } }, completeExpand: function(post) { - var bottom, file, imageBottom, oldHeight; + var bottom, file, imageBottom, oldHeight, scrollY; file = post.file; if (!file.isExpanding) { return; } - bottom = post.nodes.root.getBoundingClientRect().bottom; + bottom = Header.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; oldHeight = d.body.clientHeight; + scrollY = window.scrollY; $.addClass(post.nodes.root, 'expanded-image'); $.rmClass(file.thumb, 'expanding'); file.isExpanded = true; delete file.isExpanding; if (doc.contains(post.nodes.root) && bottom <= 0) { - window.scrollBy(0, d.body.clientHeight - oldHeight); + window.scroll(window.scrollX, scrollY + d.body.clientHeight - oldHeight); } if (file.scrollIntoView) { delete file.scrollIntoView; @@ -9207,7 +9280,7 @@ ImageCommon.rewind(this); } el.id = 'ihover'; - $.after(Header.hover, el); + $.add(Header.hover, el); if (isVideo) { el.loop = true; el.controls = false; @@ -11143,7 +11216,7 @@ return new Notice('info', "The thread is " + change + ".", 30); }, parse: function(postObjects) { - var OP, count, files, index, ipCountEl, node, num, post, postObject, posts, root, scroll, _i, _j, _len, _len1; + var OP, count, files, index, ipCountEl, node, num, post, postObject, posts, scroll, _i, _j, _len, _len1; OP = postObjects[0]; Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler; ThreadUpdater.thread.setStatus('Archived', !!+OP.archived); @@ -11206,13 +11279,8 @@ scroll = Conf['Auto Scroll'] && ThreadUpdater.scrollBG() && ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25; for (_j = 0, _len1 = posts.length; _j < _len1; _j++) { post = posts[_j]; - root = post.nodes.root; - if (post.cb) { - if (!post.cb()) { - $.add(ThreadUpdater.root, root); - } - } else { - $.add(ThreadUpdater.root, root); + if (!QuoteThreading.insert(post)) { + $.add(ThreadUpdater.root, post.nodes.root); } } $.event('PostsInserted'); @@ -11475,7 +11543,7 @@ defaultValue: 0 }); unread = quotingYou = 0; - _ref = this.response.posts.slice(1); + _ref = this.response.posts; for (_i = 0, _len = _ref.length; _i < _len; _i++) { postObj = _ref[_i]; if (!(postObj.no > lastReadPost)) { @@ -11811,68 +11879,76 @@ this.hr = $.el('hr', { id: 'unread-line' }); - this.posts = new RandomAccessList; - this.postsQuotingYou = {}; - return Thread.callbacks.push({ + this.posts = new Set; + this.postsQuotingYou = new Set; + this.order = new RandomAccessList; + this.position = null; + Thread.callbacks.push({ name: 'Unread', cb: this.node }); + return Post.callbacks.push({ + name: 'Unread', + cb: this.addPost + }); }, node: function() { + var ID, _i, _len, _ref; Unread.thread = this; Unread.title = d.title; - Unread.db.forceSync(); Unread.lastReadPost = Unread.db.get({ boardID: this.board.ID, threadID: this.ID, defaultValue: 0 }); + Unread.readCount = 0; + _ref = this.posts.keys; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + ID = _ref[_i]; + if (+ID <= Unread.lastReadPost) { + Unread.readCount++; + } + } $.one(d, '4chanXInitFinished', Unread.ready); $.on(d, 'ThreadUpdate', Unread.onUpdate); $.on(d, 'scroll visibilitychange', Unread.read); - if (Conf['Unread Line'] && !Conf['Quote Threading']) { + if (Conf['Unread Line']) { return $.on(d, 'visibilitychange', Unread.setLine); } }, ready: function() { - var posts; - if (!Conf['Quote Threading']) { - posts = []; - Unread.thread.posts.forEach(function(post) { - if (post.isReply) { - return posts.push(post); - } - }); - Unread.addPosts(posts); - } - if (Conf['Quote Threading']) { - QuoteThreading.force(); - } - if (Conf['Scroll to Last Read Post'] && !Conf['Quote Threading']) { + Unread.setLine(true); + Unread.read(); + Unread.update(); + if (Conf['Scroll to Last Read Post']) { return Unread.scroll(); } }, + positionPrev: function() { + if (Unread.position) { + return Unread.position.prev; + } else { + return Unread.order.last; + } + }, scroll: function() { - var ID, hash, posts, root, _i, _ref; + var hash, position, root; if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; } - posts = Unread.thread.posts; - _ref = posts.keys; - for (_i = _ref.length - 1; _i >= 0; _i += -1) { - ID = _ref[_i]; - if (!(+ID <= Unread.lastReadPost)) { - continue; - } - root = posts[ID].nodes.root; - if (root.getBoundingClientRect().height) { + position = Unread.positionPrev(); + while (position) { + root = position.data.nodes.root; + if (!root.getBoundingClientRect().height) { + position = position.prev; + } else { Header.scrollToIfNeeded(root, true); break; } } }, sync: function() { - var ID, lastReadPost, _i, _len, _ref; + var ID, i, lastReadPost, postIDs, _i, _ref, _ref1; if (Unread.lastReadPost == null) { return; } @@ -11885,44 +11961,36 @@ return; } Unread.lastReadPost = lastReadPost; - _ref = Unread.thread.posts.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - ID = _ref[_i]; - if (+ID > Unread.lastReadPost) { + postIDs = Unread.thread.posts.keys; + for (i = _i = _ref = Unread.readCount, _ref1 = postIDs.length; _i < _ref1; i = _i += 1) { + ID = +postIDs[i]; + if (ID > Unread.lastReadPost) { break; } - Unread.posts.rm(ID); - delete Unread.postsQuotingYou[ID]; - } - if (Conf['Unread Line'] && !Conf['Quote Threading']) { - Unread.setLine(); + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + Unread.readCount++; } + Unread.updatePosition(); + Unread.setLine(); return Unread.update(); }, - addPost: function(post) { + addPost: function() { var _ref; - if (post.ID <= Unread.lastReadPost || post.isHidden || ((_ref = QR.db) != null ? _ref.get({ - boardID: post.board.ID, - threadID: post.thread.ID, - postID: post.ID + if (this.isFetchedQuote || this.isClone) { + return; + } + Unread.order.push(this); + if (this.ID <= Unread.lastReadPost || this.isHidden || ((_ref = QR.db) != null ? _ref.get({ + boardID: this.board.ID, + threadID: this.thread.ID, + postID: this.ID }) : void 0)) { return; } - Unread.posts.push(post); - return Unread.addPostQuotingYou(post); - }, - addPosts: function(posts) { - var oldCount, post, _i, _len; - oldCount = Unread.posts.length; - for (_i = 0, _len = posts.length; _i < _len; _i++) { - post = posts[_i]; - Unread.addPost(post); - } - if (Conf['Unread Line'] && !Conf['Quote Threading']) { - Unread.setLine(oldCount === 0 && Unread.posts.length !== 0); - } - Unread.read(); - return Unread.update(); + Unread.posts.add(this.ID); + Unread.addPostQuotingYou(this); + return Unread.position != null ? Unread.position : Unread.position = Unread.order[this.ID]; }, addPostQuotingYou: function(post) { var quotelink, _i, _len, _ref, _ref1; @@ -11932,7 +12000,7 @@ if (!((_ref1 = QR.db) != null ? _ref1.get(Get.postDataFromLink(quotelink)) : void 0)) { continue; } - Unread.postsQuotingYou[post.ID] = post; + Unread.postsQuotingYou.add(post.ID); Unread.openNotification(post); return; } @@ -11957,80 +12025,76 @@ }; }, onUpdate: function(e) { - var fullID; - if (e.detail[404]) { - return Unread.update(); - } else if (!QuoteThreading.enabled) { - return Unread.addPosts((function() { - var _i, _len, _ref, _results; - _ref = e.detail.newPosts; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - fullID = _ref[_i]; - _results.push(g.posts[fullID]); - } - return _results; - })()); - } else { + if (!e.detail[404]) { + Unread.setLine(); Unread.read(); - return Unread.update(); } + return Unread.update(); }, readSinglePost: function(post) { - var ID, posts; + var ID; ID = post.ID; - posts = Unread.posts; - if (!posts[ID]) { + if (!Unread.posts.has(ID)) { return; } - posts.rm(ID); - delete Unread.postsQuotingYou[ID]; + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + Unread.updatePosition(); Unread.saveLastReadPost(); return Unread.update(); }, read: $.debounce(100, function(e) { - var ID, count, data, height, post, posts, _ref; - if (d.hidden || !Unread.posts.length) { + var ID, count, data, height, root, _ref, _ref1; + if (d.hidden || !Unread.posts.size) { return; } height = doc.clientHeight; - posts = Unread.posts; count = 0; - while (post = posts.first) { - if (!(Header.getBottomOf(post.data.nodes.root) > -1)) { + while (Unread.position) { + _ref = Unread.position, ID = _ref.ID, data = _ref.data; + root = data.nodes.root; + if (!(!root.getBoundingClientRect().height || Header.getBottomOf(root) > -1)) { break; } - ID = post.ID, data = post.data; count++; - posts.rm(ID); - delete Unread.postsQuotingYou[ID]; - if (Conf['Mark Quotes of You'] && ((_ref = QR.db) != null ? _ref.get({ + Unread.posts["delete"](ID); + Unread.postsQuotingYou["delete"](ID); + if (Conf['Mark Quotes of You'] && ((_ref1 = QR.db) != null ? _ref1.get({ boardID: data.board.ID, threadID: data.thread.ID, postID: ID }) : void 0)) { - QuoteYou.lastRead = data.nodes.root; + QuoteYou.lastRead = root; } + Unread.position = Unread.position.next; } if (!count) { return; } + Unread.updatePosition(); Unread.saveLastReadPost(); if (e) { return Unread.update(); } }), + updatePosition: function() { + var _results; + _results = []; + while (Unread.position && !Unread.posts.has(Unread.position.ID)) { + _results.push(Unread.position = Unread.position.next); + } + return _results; + }, saveLastReadPost: $.debounce(2 * $.SECOND, function() { - var ID, _i, _len, _ref; - _ref = Unread.thread.posts.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - ID = _ref[_i]; - if (Unread.posts[ID]) { + var ID, i, postIDs, _i, _ref, _ref1; + postIDs = Unread.thread.posts.keys; + for (i = _i = _ref = Unread.readCount, _ref1 = postIDs.length; _i < _ref1; i = _i += 1) { + ID = +postIDs[i]; + if (Unread.posts.has(ID)) { break; } - if (+ID > Unread.lastReadPost) { - Unread.lastReadPost = +ID; - } + Unread.lastReadPost = ID; + Unread.readCount++; } if (Unread.thread.isDead && !Unread.thread.isArchived) { return; @@ -12043,26 +12107,22 @@ }); }), setLine: function(force) { - var ID, posts, _i, _ref; - if (!(d.hidden || force === true)) { + if (!Conf['Unread Line']) { return; } - if (!Unread.posts.length) { - return $.rm(Unread.hr); - } - posts = Unread.thread.posts; - _ref = posts.keys; - for (_i = _ref.length - 1; _i >= 0; _i += -1) { - ID = _ref[_i]; - if (+ID <= Unread.lastReadPost) { - return $.after(posts[ID].nodes.root, Unread.hr); + if (d.hidden || (force === true)) { + if (Unread.linePosition = Unread.positionPrev()) { + $.after(Unread.linePosition.data.nodes.root, Unread.hr); + } else { + $.rm(Unread.hr); } } + return Unread.hr.hidden = Unread.linePosition === Unread.order.last; }, update: function() { var count, countQuotingYou, titleCount, titleDead, titleQuotingYou; - count = Unread.posts.length; - countQuotingYou = Object.keys(Unread.postsQuotingYou).length; + count = Unread.posts.size; + countQuotingYou = Unread.postsQuotingYou.size; if (Conf['Unread Count']) { titleQuotingYou = Conf['Quoted Title'] && countQuotingYou ? '(!) ' : ''; titleCount = count || !Conf['Hide Unread Count at (0)'] ? "(" + count + ") " : ''; @@ -13854,7 +13914,7 @@ className: 'dialog' }); $.extend(dialog, { - innerHTML: "
" + innerHTML: "
" }); $.on($('.export', Settings.dialog), 'click', Settings["export"]); $.on($('.import', Settings.dialog), 'click', Settings["import"]); @@ -14715,6 +14775,11 @@ } Main.callbackNodes(Thread, threads); return Main.callbackNodesDB(Post, posts, function() { + var post, _k, _len2; + for (_k = 0, _len2 = posts.length; _k < _len2; _k++) { + post = posts[_k]; + QuoteThreading.insert(post); + } return $.event('4chanXInitFinished'); }); } else { diff --git a/builds/4chan-X.zip b/builds/4chan-X.zip index e3c55413f..efe62b25e 100644 Binary files a/builds/4chan-X.zip and b/builds/4chan-X.zip differ diff --git a/builds/updates-beta.xml b/builds/updates-beta.xml index e06d9c4d4..11ef487dc 100644 --- a/builds/updates-beta.xml +++ b/builds/updates-beta.xml @@ -1,7 +1,7 @@ - + diff --git a/builds/updates.xml b/builds/updates.xml index 834a3e9dd..31c1bce7a 100644 --- a/builds/updates.xml +++ b/builds/updates.xml @@ -1,7 +1,7 @@ - + diff --git a/package.json b/package.json index b5f4f04af..59f4ba380 100755 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "Cross-browser userscript for maximum lurking on 4chan.", "meta": { "name": "4chan X", - "version": "1.9.13.4", + "version": "1.9.14.0", "repo": "https://github.com/ccd0/4chan-x/", "page": "https://github.com/ccd0/4chan-x", "downloads": "https://ccd0.github.io/4chan-x/builds/",