diff --git a/LICENSE b/LICENSE index 303f4b90c..9b1cd14c9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ /* -* appchan x - Version 2.7.5 - 2014-01-04 +* appchan x - Version 2.7.5 - 2014-01-06 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index f739ebaff..463799254 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -22,7 +22,7 @@ // ==/UserScript== /* -* appchan x - Version 2.7.5 - 2014-01-04 +* appchan x - Version 2.7.5 - 2014-01-06 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE @@ -110,7 +110,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, InfiniScroll, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, InfiniScroll, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, 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, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __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; }, __slice = [].slice, __hasProp = {}.hasOwnProperty, @@ -3825,6 +3825,99 @@ })(); + RandomAccessList = (function() { + function RandomAccessList() { + this.length = 0; + } + + RandomAccessList.prototype.push = function(item) { + var ID, last; + ID = item.ID; + if (this[ID]) { + return; + } + last = this.last; + item.prev = last; + this[ID] = item; + this.last = last ? last.next = item : this.first = item; + return this.length++; + }; + + RandomAccessList.prototype.after = function(root, item) { + var next; + if (item.prev === root) { + return; + } + this.rmi(item); + next = root.next; + root.next = item; + item.prev = root; + item.next = next; + return next.prev = item; + }; + + RandomAccessList.prototype.prepend = function(item) { + var first; + first = this.first; + if (item === first || !this[item.ID]) { + return; + } + this.rmi(item); + item.next = first; + first.prev = item; + this.first = item; + return delete item.prev; + }; + + RandomAccessList.prototype.shift = function() { + return this.rm(this.first.ID); + }; + + RandomAccessList.prototype.rm = function(ID) { + var item; + item = this[ID]; + if (!item) { + return; + } + delete this[ID]; + this.length--; + this.rmi(item); + delete item.next; + return delete item.prev; + }; + + RandomAccessList.prototype.rmi = function(item) { + var next, prev; + prev = item.prev, next = item.next; + if (prev) { + prev.next = next; + } else { + this.first = next; + } + if (next) { + return next.prev = prev; + } else { + return this.last = prev; + } + }; + + RandomAccessList.prototype.closest = function(ID) { + var item, prev; + item = this.first; + while (item) { + if (item.ID > ID) { + prev = item.prev; + break; + } + item = item.next; + } + return (prev ? prev.ID : -1); + }; + + return RandomAccessList; + + })(); + Polyfill = { init: function() {}, notificationPermission: function() { @@ -7126,108 +7219,120 @@ innerHTML: '' }); input = $('input', this.controls); - $.on(input, 'change', QuoteThreading.toggle); + $.on(input, 'change', this.toggle); $.event('AddMenuEntry', { type: 'header', el: this.controls, order: 98 }); - $.on(d, '4chanXInitFinished', this.setup); + if (!Conf['Unread Count']) { + $.on(d, '4chanXInitFinished', this.setup); + } return Post.callbacks.push({ name: 'Quote Threading', cb: this.node }); }, setup: function() { - var ID, post, posts; + var ID, post, _ref; $.off(d, '4chanXInitFinished', QuoteThreading.setup); - posts = g.posts; - for (ID in posts) { - post = posts[ID]; + _ref = g.posts; + for (ID in _ref) { + post = _ref[ID]; if (post.cb) { - post.cb.call(post); + post.cb(); } } return QuoteThreading.hasRun = true; }, node: function() { - var ID, fullID, keys, len, post, posts, qid, quote, quotes, uniq, _i, _len; - if (this.isClone || !QuoteThreading.enabled || this.thread.OP === this) { - return; - } - quotes = this.quotes, ID = this.ID, fullID = this.fullID; + var keys, len, post, posts, quote, _i, _len, _ref; posts = g.posts; - if (!(post = posts[fullID]) || post.isHidden) { + if (this.isClone || !QuoteThreading.enabled) { return; } - uniq = {}; - len = ("" + g.BOARD).length + 1; - for (_i = 0, _len = quotes.length; _i < _len; _i++) { - quote = quotes[_i]; - qid = quote; - if (!(qid.slice(len) < ID)) { - continue; - } - if (qid in posts) { - uniq[qid.slice(len)] = true; + if (Conf['Unread Count']) { + Unread.posts.push(this); + } + if (this.thread.OP === this || !(post = posts[this.fullID]) || post.isHidden) { + return; + } + 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); } } - keys = Object.keys(uniq); if (keys.length !== 1) { return; } - this.threaded = "" + g.BOARD + "." + keys[0]; + this.threaded = keys[0]; return this.cb = QuoteThreading.nodeinsert; }, nodeinsert: function() { - var bottom, height, qpost, qroot, threadContainer, top, _ref; - qpost = g.posts[this.threaded]; - delete this.threaded; - delete this.cb; - if (this.thread.OP === qpost) { + var ID, bottom, height, post, posts, root, threadContainer, top, _ref; + post = g.posts[this.threaded]; + posts = Unread.posts; + if (this.thread.OP === post) { return false; } if (QuoteThreading.hasRun) { height = doc.clientHeight; - _ref = qpost.nodes.root.getBoundingClientRect(), bottom = _ref.bottom, top = _ref.top; - if (!(__indexOf.call(Unread.posts, qpost) >= 0 || ((bottom < height) && (top > 0)))) { + _ref = post.nodes.root.getBoundingClientRect(), bottom = _ref.bottom, top = _ref.top; + if (!((posts != null ? posts[post.ID] : void 0) || ((bottom < height) && (top > 0)))) { return false; } } - qroot = qpost.nodes.root; - if (!$.hasClass(qroot, 'threadOP')) { - $.addClass(qroot, 'threadOP'); + root = post.nodes.root; + if (!$.hasClass(root, 'threadOP')) { + $.addClass(root, 'threadOP'); threadContainer = $.el('div', { className: 'threadContainer' }); - $.after(qroot, threadContainer); + $.after(root, threadContainer); } else { - threadContainer = qroot.nextSibling; + threadContainer = root.nextSibling; + post = Get.postFromRoot($.x('descendant::div[contains(@class,"postContainer")][last()]', threadContainer)); } $.add(threadContainer, this.nodes.root); + if (!Conf['Unread Count']) { + return true; + } + if (posts[post.ID]) { + posts.after(post, this); + return true; + } + if ((ID = posts.closest(post.ID)) !== -1) { + posts.after(posts[ID], this); + } else { + posts.prepend(this); + } return true; }, toggle: function() { - var container, containers, node, post, replies, reply, thread, _i, _j, _k, _len, _len1, _len2, _ref; + var container, containers, post, replies, reply, thread, _i, _j, _k, _len, _len1, _len2, _ref; + if (Conf['Unread Count']) { + Unread.posts = new RandomAccessList; + Unread.ready(); + } thread = $('.thread'); replies = $$('.thread > .replyContainer, .threadContainer > .replyContainer', thread); - QuoteThreading.enabled = this.checked; - if (this.checked) { + if (QuoteThreading.enabled = this.checked) { QuoteThreading.hasRun = false; for (_i = 0, _len = replies.length; _i < _len; _i++) { reply = replies[_i]; - QuoteThreading.node.call(node = Get.postFromRoot(reply)); - if (node.cb) { - node.cb(); + post = Get.postFromRoot(reply); + if (post.cb) { + post.cb(); } } QuoteThreading.hasRun = true; } else { replies.sort(function(a, b) { - var aID, bID; - aID = Number(a.id.slice(2)); - bID = Number(b.id.slice(2)); - return aID - bID; + return Number(a.id.slice(2)) - Number(b.id.slice(2)); }); $.add(thread, replies); containers = $$('.threadContainer', thread); @@ -7241,12 +7346,15 @@ $.rmClass(post, 'threadOP'); } } - return Unread.update(true); + if (Conf['Unread Count']) { + return Unread.read(); + } }, kb: function() { var control; control = $.id('threadingControl'); - return control.click(); + control.checked = !control.checked; + return QuoteThreading.toggle.call(control); } }; @@ -11168,7 +11276,7 @@ } root = post.nodes.root; if (post.cb) { - if (!post.cb.call(post)) { + if (!post.cb()) { $.add(ThreadUpdater.root, root); } } else { @@ -11698,8 +11806,19 @@ this.hr = $.el('hr', { id: 'unread-line' }); - this.posts = []; + this.posts = new RandomAccessList; this.postsQuotingYou = []; + this.qr = QR.db ? function(post) { + var data; + data = { + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + }; + return (QR.db.get(data) ? true : false); + } : function() { + return false; + }; return Thread.callbacks.push({ name: 'Unread', cb: this.node @@ -11732,17 +11851,19 @@ } } Unread.addPosts(posts); - return Unread.scroll(); + if (Conf['Quote Threading']) { + QuoteThreading.setup(); + } + if (Conf['Scroll to Last Read Post']) { + return Unread.scroll(); + } }, scroll: function() { var down, hash, post, posts, root; - if (!Conf['Scroll to Last Read Post']) { - return; - } if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; } - if (post = Unread.posts[0]) { + if (post = Unread.posts.first) { while (root = $.x('preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root)) { if (!(post = Get.postFromRoot(root)).isHidden) { break; @@ -11761,7 +11882,7 @@ } }, sync: function() { - var lastReadPost; + var ID, lastReadPost, post; lastReadPost = Unread.db.get({ boardID: Unread.thread.board.ID, threadID: Unread.thread.ID, @@ -11771,7 +11892,14 @@ return; } Unread.lastReadPost = lastReadPost; - Unread.readArray(Unread.posts); + post = Unread.posts.first; + while (post) { + if ((ID = post.ID, post) > Unread.lastReadPost) { + break; + } + post = post.next; + Unread.posts.rm(ID); + } Unread.readArray(Unread.postsQuotingYou); if (Conf['Unread Line']) { Unread.setLine(); @@ -11779,28 +11907,20 @@ return Unread.update(); }, addPosts: function(posts) { - var ID, data, post, _i, _len, _ref; + var ID, post, _i, _len, _ref; for (_i = 0, _len = posts.length; _i < _len; _i++) { post = posts[_i]; ID = post.ID; - if (ID <= Unread.lastReadPost || post.isHidden) { + if (ID <= Unread.lastReadPost || post.isHidden || Unread.qr(post)) { continue; } - if (QR.db) { - data = { - boardID: post.board.ID, - threadID: post.thread.ID, - postID: post.ID - }; - if (QR.db.get(data)) { - continue; - } + if (!(post.prev || post.next)) { + Unread.posts.push(post); } - Unread.posts.push(post); Unread.addPostQuotingYou(post); } if (Conf['Unread Line']) { - Unread.setLine((_ref = Unread.posts[0], __indexOf.call(posts, _ref) >= 0)); + Unread.setLine((_ref = Unread.posts.first, __indexOf.call(posts, _ref) >= 0)); } Unread.read(); return Unread.update(); @@ -11849,15 +11969,16 @@ } }, readSinglePost: function(post) { - var i; - if ((i = Unread.posts.indexOf(post)) === -1) { + var ID, i; + ID = post.ID; + if (!Unread.posts[ID]) { return; } - Unread.posts.splice(i, 1); - if (i === 0) { - Unread.lastReadPost = post.ID; + if (post === Unread.posts.first) { + Unread.lastReadPost = ID; Unread.saveLastReadPost(); } + Unread.posts.rm(ID); if ((i = Unread.postsQuotingYou.indexOf(post)) !== -1) { Unread.postsQuotingYou.splice(i, 1); } @@ -11873,35 +11994,22 @@ } return arr.splice(0, i); }, - read: $.debounce(50, function(e) { - var ID, height, i, post, posts; + read: $.debounce(100, function(e) { + var ID, height, post, posts; if (d.hidden || !Unread.posts.length) { return; } height = doc.clientHeight; posts = Unread.posts; - i = 0; - while (post = posts[i]) { - if (Header.getBottomOf(post.nodes.root) > -1) { - ID = post.ID; - if (Conf['Mark Quotes of You']) { - if (post.info.yours) { - QuoteYou.lastRead = post.nodes.root; - } - } - if (Conf['Quote Threading']) { - posts.splice(i, 1); - continue; - } - } else { - if (!Conf['Quote Threading']) { - break; - } + while (post = posts.first) { + if (!(Header.getBottomOf(post.nodes.root) > -1)) { + break; + } + ID = post.ID; + posts.rm(ID); + if (Conf['Mark Quotes of You'] && post.info.yours) { + QuoteYou.lastRead = post.nodes.root; } - i++; - } - if (i && !Conf['Quote Threading']) { - posts.splice(0, i); } if (!ID) { return; @@ -11930,7 +12038,7 @@ if (!(d.hidden || force === true)) { return; } - if (!(post = Unread.posts[0])) { + if (!(post = Unread.posts.first)) { return $.rm(Unread.hr); } if ($.x('preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root)) { diff --git a/builds/crx/script.js b/builds/crx/script.js index b037fa137..0783e0426 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript /* -* appchan x - Version 2.7.5 - 2014-01-04 +* appchan x - Version 2.7.5 - 2014-01-06 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE @@ -88,7 +88,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, InfiniScroll, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Color, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, InfiniScroll, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, 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, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __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; }, __slice = [].slice, __hasProp = {}.hasOwnProperty, @@ -3832,6 +3832,99 @@ })(); + RandomAccessList = (function() { + function RandomAccessList() { + this.length = 0; + } + + RandomAccessList.prototype.push = function(item) { + var ID, last; + ID = item.ID; + if (this[ID]) { + return; + } + last = this.last; + item.prev = last; + this[ID] = item; + this.last = last ? last.next = item : this.first = item; + return this.length++; + }; + + RandomAccessList.prototype.after = function(root, item) { + var next; + if (item.prev === root) { + return; + } + this.rmi(item); + next = root.next; + root.next = item; + item.prev = root; + item.next = next; + return next.prev = item; + }; + + RandomAccessList.prototype.prepend = function(item) { + var first; + first = this.first; + if (item === first || !this[item.ID]) { + return; + } + this.rmi(item); + item.next = first; + first.prev = item; + this.first = item; + return delete item.prev; + }; + + RandomAccessList.prototype.shift = function() { + return this.rm(this.first.ID); + }; + + RandomAccessList.prototype.rm = function(ID) { + var item; + item = this[ID]; + if (!item) { + return; + } + delete this[ID]; + this.length--; + this.rmi(item); + delete item.next; + return delete item.prev; + }; + + RandomAccessList.prototype.rmi = function(item) { + var next, prev; + prev = item.prev, next = item.next; + if (prev) { + prev.next = next; + } else { + this.first = next; + } + if (next) { + return next.prev = prev; + } else { + return this.last = prev; + } + }; + + RandomAccessList.prototype.closest = function(ID) { + var item, prev; + item = this.first; + while (item) { + if (item.ID > ID) { + prev = item.prev; + break; + } + item = item.next; + } + return (prev ? prev.ID : -1); + }; + + return RandomAccessList; + + })(); + Polyfill = { init: function() { this.notificationPermission(); @@ -7130,108 +7223,120 @@ innerHTML: '' }); input = $('input', this.controls); - $.on(input, 'change', QuoteThreading.toggle); + $.on(input, 'change', this.toggle); $.event('AddMenuEntry', { type: 'header', el: this.controls, order: 98 }); - $.on(d, '4chanXInitFinished', this.setup); + if (!Conf['Unread Count']) { + $.on(d, '4chanXInitFinished', this.setup); + } return Post.callbacks.push({ name: 'Quote Threading', cb: this.node }); }, setup: function() { - var ID, post, posts; + var ID, post, _ref; $.off(d, '4chanXInitFinished', QuoteThreading.setup); - posts = g.posts; - for (ID in posts) { - post = posts[ID]; + _ref = g.posts; + for (ID in _ref) { + post = _ref[ID]; if (post.cb) { - post.cb.call(post); + post.cb(); } } return QuoteThreading.hasRun = true; }, node: function() { - var ID, fullID, keys, len, post, posts, qid, quote, quotes, uniq, _i, _len; - if (this.isClone || !QuoteThreading.enabled || this.thread.OP === this) { - return; - } - quotes = this.quotes, ID = this.ID, fullID = this.fullID; + var keys, len, post, posts, quote, _i, _len, _ref; posts = g.posts; - if (!(post = posts[fullID]) || post.isHidden) { + if (this.isClone || !QuoteThreading.enabled) { return; } - uniq = {}; - len = ("" + g.BOARD).length + 1; - for (_i = 0, _len = quotes.length; _i < _len; _i++) { - quote = quotes[_i]; - qid = quote; - if (!(qid.slice(len) < ID)) { - continue; - } - if (qid in posts) { - uniq[qid.slice(len)] = true; + if (Conf['Unread Count']) { + Unread.posts.push(this); + } + if (this.thread.OP === this || !(post = posts[this.fullID]) || post.isHidden) { + return; + } + 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); } } - keys = Object.keys(uniq); if (keys.length !== 1) { return; } - this.threaded = "" + g.BOARD + "." + keys[0]; + this.threaded = keys[0]; return this.cb = QuoteThreading.nodeinsert; }, nodeinsert: function() { - var bottom, height, qpost, qroot, threadContainer, top, _ref; - qpost = g.posts[this.threaded]; - delete this.threaded; - delete this.cb; - if (this.thread.OP === qpost) { + var ID, bottom, height, post, posts, root, threadContainer, top, _ref; + post = g.posts[this.threaded]; + posts = Unread.posts; + if (this.thread.OP === post) { return false; } if (QuoteThreading.hasRun) { height = doc.clientHeight; - _ref = qpost.nodes.root.getBoundingClientRect(), bottom = _ref.bottom, top = _ref.top; - if (!(__indexOf.call(Unread.posts, qpost) >= 0 || ((bottom < height) && (top > 0)))) { + _ref = post.nodes.root.getBoundingClientRect(), bottom = _ref.bottom, top = _ref.top; + if (!((posts != null ? posts[post.ID] : void 0) || ((bottom < height) && (top > 0)))) { return false; } } - qroot = qpost.nodes.root; - if (!$.hasClass(qroot, 'threadOP')) { - $.addClass(qroot, 'threadOP'); + root = post.nodes.root; + if (!$.hasClass(root, 'threadOP')) { + $.addClass(root, 'threadOP'); threadContainer = $.el('div', { className: 'threadContainer' }); - $.after(qroot, threadContainer); + $.after(root, threadContainer); } else { - threadContainer = qroot.nextSibling; + threadContainer = root.nextSibling; + post = Get.postFromRoot($.x('descendant::div[contains(@class,"postContainer")][last()]', threadContainer)); } $.add(threadContainer, this.nodes.root); + if (!Conf['Unread Count']) { + return true; + } + if (posts[post.ID]) { + posts.after(post, this); + return true; + } + if ((ID = posts.closest(post.ID)) !== -1) { + posts.after(posts[ID], this); + } else { + posts.prepend(this); + } return true; }, toggle: function() { - var container, containers, node, post, replies, reply, thread, _i, _j, _k, _len, _len1, _len2, _ref; + var container, containers, post, replies, reply, thread, _i, _j, _k, _len, _len1, _len2, _ref; + if (Conf['Unread Count']) { + Unread.posts = new RandomAccessList; + Unread.ready(); + } thread = $('.thread'); replies = $$('.thread > .replyContainer, .threadContainer > .replyContainer', thread); - QuoteThreading.enabled = this.checked; - if (this.checked) { + if (QuoteThreading.enabled = this.checked) { QuoteThreading.hasRun = false; for (_i = 0, _len = replies.length; _i < _len; _i++) { reply = replies[_i]; - QuoteThreading.node.call(node = Get.postFromRoot(reply)); - if (node.cb) { - node.cb(); + post = Get.postFromRoot(reply); + if (post.cb) { + post.cb(); } } QuoteThreading.hasRun = true; } else { replies.sort(function(a, b) { - var aID, bID; - aID = Number(a.id.slice(2)); - bID = Number(b.id.slice(2)); - return aID - bID; + return Number(a.id.slice(2)) - Number(b.id.slice(2)); }); $.add(thread, replies); containers = $$('.threadContainer', thread); @@ -7245,12 +7350,15 @@ $.rmClass(post, 'threadOP'); } } - return Unread.update(true); + if (Conf['Unread Count']) { + return Unread.read(); + } }, kb: function() { var control; control = $.id('threadingControl'); - return control.click(); + control.checked = !control.checked; + return QuoteThreading.toggle.call(control); } }; @@ -11152,7 +11260,7 @@ } root = post.nodes.root; if (post.cb) { - if (!post.cb.call(post)) { + if (!post.cb()) { $.add(ThreadUpdater.root, root); } } else { @@ -11682,8 +11790,19 @@ this.hr = $.el('hr', { id: 'unread-line' }); - this.posts = []; + this.posts = new RandomAccessList; this.postsQuotingYou = []; + this.qr = QR.db ? function(post) { + var data; + data = { + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + }; + return (QR.db.get(data) ? true : false); + } : function() { + return false; + }; return Thread.callbacks.push({ name: 'Unread', cb: this.node @@ -11716,17 +11835,19 @@ } } Unread.addPosts(posts); - return Unread.scroll(); + if (Conf['Quote Threading']) { + QuoteThreading.setup(); + } + if (Conf['Scroll to Last Read Post']) { + return Unread.scroll(); + } }, scroll: function() { var down, hash, post, posts, root; - if (!Conf['Scroll to Last Read Post']) { - return; - } if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; } - if (post = Unread.posts[0]) { + if (post = Unread.posts.first) { while (root = $.x('preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root)) { if (!(post = Get.postFromRoot(root)).isHidden) { break; @@ -11745,7 +11866,7 @@ } }, sync: function() { - var lastReadPost; + var ID, lastReadPost, post; lastReadPost = Unread.db.get({ boardID: Unread.thread.board.ID, threadID: Unread.thread.ID, @@ -11755,7 +11876,14 @@ return; } Unread.lastReadPost = lastReadPost; - Unread.readArray(Unread.posts); + post = Unread.posts.first; + while (post) { + if ((ID = post.ID, post) > Unread.lastReadPost) { + break; + } + post = post.next; + Unread.posts.rm(ID); + } Unread.readArray(Unread.postsQuotingYou); if (Conf['Unread Line']) { Unread.setLine(); @@ -11763,28 +11891,20 @@ return Unread.update(); }, addPosts: function(posts) { - var ID, data, post, _i, _len, _ref; + var ID, post, _i, _len, _ref; for (_i = 0, _len = posts.length; _i < _len; _i++) { post = posts[_i]; ID = post.ID; - if (ID <= Unread.lastReadPost || post.isHidden) { + if (ID <= Unread.lastReadPost || post.isHidden || Unread.qr(post)) { continue; } - if (QR.db) { - data = { - boardID: post.board.ID, - threadID: post.thread.ID, - postID: post.ID - }; - if (QR.db.get(data)) { - continue; - } + if (!(post.prev || post.next)) { + Unread.posts.push(post); } - Unread.posts.push(post); Unread.addPostQuotingYou(post); } if (Conf['Unread Line']) { - Unread.setLine((_ref = Unread.posts[0], __indexOf.call(posts, _ref) >= 0)); + Unread.setLine((_ref = Unread.posts.first, __indexOf.call(posts, _ref) >= 0)); } Unread.read(); return Unread.update(); @@ -11833,15 +11953,16 @@ } }, readSinglePost: function(post) { - var i; - if ((i = Unread.posts.indexOf(post)) === -1) { + var ID, i; + ID = post.ID; + if (!Unread.posts[ID]) { return; } - Unread.posts.splice(i, 1); - if (i === 0) { - Unread.lastReadPost = post.ID; + if (post === Unread.posts.first) { + Unread.lastReadPost = ID; Unread.saveLastReadPost(); } + Unread.posts.rm(ID); if ((i = Unread.postsQuotingYou.indexOf(post)) !== -1) { Unread.postsQuotingYou.splice(i, 1); } @@ -11857,35 +11978,22 @@ } return arr.splice(0, i); }, - read: $.debounce(50, function(e) { - var ID, height, i, post, posts; + read: $.debounce(100, function(e) { + var ID, height, post, posts; if (d.hidden || !Unread.posts.length) { return; } height = doc.clientHeight; posts = Unread.posts; - i = 0; - while (post = posts[i]) { - if (Header.getBottomOf(post.nodes.root) > -1) { - ID = post.ID; - if (Conf['Mark Quotes of You']) { - if (post.info.yours) { - QuoteYou.lastRead = post.nodes.root; - } - } - if (Conf['Quote Threading']) { - posts.splice(i, 1); - continue; - } - } else { - if (!Conf['Quote Threading']) { - break; - } + while (post = posts.first) { + if (!(Header.getBottomOf(post.nodes.root) > -1)) { + break; + } + ID = post.ID; + posts.rm(ID); + if (Conf['Mark Quotes of You'] && post.info.yours) { + QuoteYou.lastRead = post.nodes.root; } - i++; - } - if (i && !Conf['Quote Threading']) { - posts.splice(0, i); } if (!ID) { return; @@ -11914,7 +12022,7 @@ if (!(d.hidden || force === true)) { return; } - if (!(post = Unread.posts[0])) { + if (!(post = Unread.posts.first)) { return $.rm(Unread.hr); } if ($.x('preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root)) { diff --git a/src/General/lib/classes.coffee b/src/General/lib/classes.coffee index 74697eec3..63c33645e 100755 --- a/src/General/lib/classes.coffee +++ b/src/General/lib/classes.coffee @@ -3,4 +3,5 @@ <%= grunt.file.read('src/General/lib/post.class') %> <%= grunt.file.read('src/General/lib/clone.class') %> <%= grunt.file.read('src/General/lib/databoard.class') %> -<%= grunt.file.read('src/General/lib/notice.class') %> \ No newline at end of file +<%= grunt.file.read('src/General/lib/notice.class') %> +<%= grunt.file.read('src/General/lib/randomaccesslist.class') %> \ No newline at end of file diff --git a/src/General/lib/randomaccesslist.class b/src/General/lib/randomaccesslist.class new file mode 100644 index 000000000..98c08a670 --- /dev/null +++ b/src/General/lib/randomaccesslist.class @@ -0,0 +1,67 @@ +class RandomAccessList + constructor: -> + @length = 0 + + push: (item) -> + {ID} = item + return if @[ID] + {last} = @ + item.prev = last + @[ID] = item + @last = if last + last.next = item + else + @first = item + @length++ + + after: (root, item) -> + return if item.prev is root + + @rmi item + + {next} = root + root.next = item + item.prev = root + item.next = next + next.prev = item + + prepend: (item) -> + {first} = @ + return if item is first or not @[item.ID] + @rmi item + item.next = first + first.prev = item + @first = item + delete item.prev + + shift: -> + @rm @first.ID + + rm: (ID) -> + item = @[ID] + return unless item + delete @[ID] + @length-- + @rmi item + delete item.next + delete item.prev + + rmi: (item) -> + {prev, next} = item + if prev + prev.next = next + else + @first = next + if next + next.prev = prev + else + @last = prev + + closest: (ID) -> + item = @first + while item + if item.ID > ID + {prev} = item + break + item = item.next + return (if prev then prev.ID else -1) \ No newline at end of file diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee index 13fb2edd8..abbadd3cd 100755 --- a/src/Monitoring/ThreadUpdater.coffee +++ b/src/Monitoring/ThreadUpdater.coffee @@ -296,7 +296,7 @@ ThreadUpdater = continue unless posts.hasOwnProperty key root = post.nodes.root if post.cb - unless post.cb.call post + unless post.cb() $.add ThreadUpdater.root, root else $.add ThreadUpdater.root, root diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee index 385383639..02c50faf5 100755 --- a/src/Monitoring/Unread.coffee +++ b/src/Monitoring/Unread.coffee @@ -5,9 +5,19 @@ Unread = @db = new DataBoard 'lastReadPosts', @sync @hr = $.el 'hr', id: 'unread-line' - @posts = [] + @posts = new RandomAccessList @postsQuotingYou = [] + @qr = if QR.db + (post) -> + data = + boardID: post.board.ID + threadID: post.thread.ID + postID: post.ID + return (if QR.db.get data then true else false) + else -> + return false + Thread.callbacks.push name: 'Unread' cb: @node @@ -27,16 +37,15 @@ Unread = ready: -> $.off d, '4chanXInitFinished', Unread.ready posts = [] - for ID, post of Unread.thread.posts - posts.push post if post.isReply + posts.push post for ID, post of Unread.thread.posts when post.isReply Unread.addPosts posts - Unread.scroll() + QuoteThreading.setup() if Conf['Quote Threading'] + Unread.scroll() if Conf['Scroll to Last Read Post'] scroll: -> - return unless Conf['Scroll to Last Read Post'] # Let the header's onload callback handle it. return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts - if post = Unread.posts[0] + if post = Unread.posts.first # Scroll to a non-hidden, non-OP post that's before the first unread post. while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root break unless (post = Get.postFromRoot root).isHidden @@ -46,7 +55,7 @@ Unread = # Scroll to the last read post. posts = Object.keys Unread.thread.posts {root} = Unread.thread.posts[posts[posts.length - 1]].nodes - + # Scroll to the target unless we scrolled past it. Header.scrollTo root, down if Header.getBottomOf(root) < 0 @@ -57,7 +66,13 @@ Unread = defaultValue: 0 return unless Unread.lastReadPost < lastReadPost Unread.lastReadPost = lastReadPost - Unread.readArray Unread.posts + + post = Unread.posts.first + while post + break if ({ID} = post) > Unread.lastReadPost + post = post.next + Unread.posts.rm ID + Unread.readArray Unread.postsQuotingYou Unread.setLine() if Conf['Unread Line'] Unread.update() @@ -65,19 +80,12 @@ Unread = addPosts: (posts) -> for post in posts {ID} = post - if ID <= Unread.lastReadPost or post.isHidden - continue - if QR.db - data = - boardID: post.board.ID - threadID: post.thread.ID - postID: post.ID - continue if QR.db.get data - Unread.posts.push post + continue if ID <= Unread.lastReadPost or post.isHidden or Unread.qr post + Unread.posts.push post unless post.prev or post.next Unread.addPostQuotingYou post if Conf['Unread Line'] # Force line on visible threads if there were no unread posts previously. - Unread.setLine Unread.posts[0] in posts + Unread.setLine Unread.posts.first in posts Unread.read() Unread.update() @@ -112,11 +120,12 @@ Unread = Unread.addPosts e.detail.newPosts readSinglePost: (post) -> - return if (i = Unread.posts.indexOf post) is -1 - Unread.posts.splice i, 1 - if i is 0 - Unread.lastReadPost = post.ID + {ID} = post + return unless Unread.posts[ID] + if post is Unread.posts.first + Unread.lastReadPost = ID Unread.saveLastReadPost() + Unread.posts.rm ID if (i = Unread.postsQuotingYou.indexOf post) isnt -1 Unread.postsQuotingYou.splice i, 1 Unread.update() @@ -126,28 +135,18 @@ Unread = break if post.ID > Unread.lastReadPost arr.splice 0, i - read: $.debounce 50, (e) -> + read: $.debounce 100, (e) -> return if d.hidden or !Unread.posts.length height = doc.clientHeight - {posts} = Unread - i = 0 - while post = posts[i] - if Header.getBottomOf(post.nodes.root) > -1 # post is not completely read - {ID} = post - if Conf['Mark Quotes of You'] - if post.info.yours - QuoteYou.lastRead = post.nodes.root - if Conf['Quote Threading'] - posts.splice i, 1 - continue - else - unless Conf['Quote Threading'] - break - i++ - - if i and !Conf['Quote Threading'] - posts.splice 0, i + {posts} = Unread + while post = posts.first + break unless Header.getBottomOf(post.nodes.root) > -1 # post is not completely read + {ID} = post + posts.rm ID + + if Conf['Mark Quotes of You'] and post.info.yours + QuoteYou.lastRead = post.nodes.root return unless ID @@ -159,13 +158,13 @@ Unread = saveLastReadPost: $.debounce 2 * $.SECOND, -> return if Unread.thread.isDead Unread.db.set - boardID: Unread.thread.board.ID + boardID: Unread.thread.board.ID threadID: Unread.thread.ID val: Unread.lastReadPost setLine: (force) -> return unless d.hidden or force is true - return $.rm Unread.hr unless post = Unread.posts[0] + return $.rm Unread.hr unless post = Unread.posts.first if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root # not the first reply $.before post.nodes.root, Unread.hr @@ -173,7 +172,7 @@ Unread = count = Unread.posts.length if Conf['Unread Count'] - d.title = "#{if Conf['Quoted Title'] and Unread.postsQuotingYou.length then '(!) ' else ''}#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}" + d.title = "#{if Conf['Quoted Title'] and Unread.postsQuotingYou.length then '(!) ' else ''}#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}" <% if (type === 'crx') { %> # XXX Chrome bug where it doesn't always update the tab title. # crbug.com/124381 diff --git a/src/Quotelinks/QuoteThreading.coffee b/src/Quotelinks/QuoteThreading.coffee index 3287c9a4f..ddc975dd0 100755 --- a/src/Quotelinks/QuoteThreading.coffee +++ b/src/Quotelinks/QuoteThreading.coffee @@ -11,14 +11,14 @@ QuoteThreading = innerHTML: '' input = $ 'input', @controls - $.on input, 'change', QuoteThreading.toggle + $.on input, 'change', @toggle $.event 'AddMenuEntry', type: 'header' el: @controls order: 98 - $.on d, '4chanXInitFinished', @setup + $.on d, '4chanXInitFinished', @setup unless Conf['Unread Count'] Post.callbacks.push name: 'Quote Threading' @@ -26,83 +26,91 @@ QuoteThreading = setup: -> $.off d, '4chanXInitFinished', QuoteThreading.setup - {posts} = g - - for ID, post of posts - if post.cb - post.cb.call post + post.cb() for ID, post of g.posts when post.cb QuoteThreading.hasRun = true node: -> - return if @isClone or not QuoteThreading.enabled or @thread.OP is @ - - {quotes, ID, fullID} = @ {posts} = g - return if !(post = posts[fullID]) or post.isHidden # Filtered + return if @isClone or not QuoteThreading.enabled + Unread.posts.push @ if Conf['Unread Count'] - uniq = {} - len = "#{g.BOARD}".length + 1 - for quote in quotes - qid = quote - continue unless qid[len..] < ID - if qid of posts - uniq[qid[len..]] = true + return if @thread.OP is @ or !(post = posts[@fullID]) or post.isHidden # Filtered + + keys = [] + len = g.BOARD.ID.length + 1 + keys.push quote for quote in @quotes when (quote[len..] < @ID) and quote of posts - keys = Object.keys uniq return unless keys.length is 1 - @threaded = "#{g.BOARD}.#{keys[0]}" + @threaded = keys[0] @cb = QuoteThreading.nodeinsert nodeinsert: -> - qpost = g.posts[@threaded] + post = g.posts[@threaded] + {posts} = Unread - delete @threaded - delete @cb - - return false if @thread.OP is qpost + return false if @thread.OP is post if QuoteThreading.hasRun height = doc.clientHeight - {bottom, top} = qpost.nodes.root.getBoundingClientRect() + {bottom, top} = post.nodes.root.getBoundingClientRect() # Post is unread or is fully visible. - return false unless qpost in Unread.posts or ((bottom < height) and (top > 0)) + return false unless posts?[post.ID] or ((bottom < height) and (top > 0)) - qroot = qpost.nodes.root - unless $.hasClass qroot, 'threadOP' - $.addClass qroot, 'threadOP' + {root} = post.nodes + unless $.hasClass root, 'threadOP' + $.addClass root, 'threadOP' threadContainer = $.el 'div', className: 'threadContainer' - $.after qroot, threadContainer + $.after root, threadContainer else - threadContainer = qroot.nextSibling + threadContainer = root.nextSibling + post = Get.postFromRoot $.x 'descendant::div[contains(@class,"postContainer")][last()]', threadContainer $.add threadContainer, @nodes.root + + return true unless Conf['Unread Count'] + + if posts[post.ID] + posts.after post, @ + return true + + if (ID = posts.closest post.ID) isnt -1 + posts.after posts[ID], @ + else + posts.prepend @ + return true toggle: -> + if Conf['Unread Count'] + Unread.posts = new RandomAccessList + Unread.ready() + thread = $ '.thread' replies = $$ '.thread > .replyContainer, .threadContainer > .replyContainer', thread - QuoteThreading.enabled = @checked - if @checked + + if QuoteThreading.enabled = @checked QuoteThreading.hasRun = false for reply in replies - QuoteThreading.node.call node = Get.postFromRoot reply - node.cb() if node.cb + post = Get.postFromRoot reply + # QuoteThreading calculates whether or not posts should be threaded based on content + # and then threads them based on thread context, so regardless of whether or not it + # actually threads them all eligible posts WILL have a cb. Magic. + post.cb() if post.cb QuoteThreading.hasRun = true + else - replies.sort (a, b) -> - aID = Number a.id[2..] - bID = Number b.id[2..] - aID - bID + replies.sort (a, b) -> Number(a.id[2..]) - Number(b.id[2..]) $.add thread, replies containers = $$ '.threadContainer', thread $.rm container for container in containers $.rmClass post, 'threadOP' for post in $$ '.threadOP' - Unread.update true + Unread.read() if Conf['Unread Count'] kb: -> control = $.id 'threadingControl' - control.click() \ No newline at end of file + control.checked = not control.checked + QuoteThreading.toggle.call control \ No newline at end of file