diff --git a/src/General/Main.coffee b/src/General/Main.coffee index ee10abd8f..360819b26 100755 --- a/src/General/Main.coffee +++ b/src/General/Main.coffee @@ -197,6 +197,7 @@ Main = Main.callbackNodes Thread, threads Main.callbackNodesDB Post, posts, -> + QuoteThreading.insert post for post in posts $.event '4chanXInitFinished' else diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee index b94cab67f..4706c4cca 100755 --- a/src/Monitoring/ThreadUpdater.coffee +++ b/src/Monitoring/ThreadUpdater.coffee @@ -320,12 +320,8 @@ ThreadUpdater = ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25 for post in posts - root = post.nodes.root - if post.cb - unless post.cb() - $.add ThreadUpdater.root, root - else - $.add ThreadUpdater.root, root + unless QuoteThreading.insert post + $.add ThreadUpdater.root, post.nodes.root $.event 'PostsInserted' if scroll diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee index 1051ff223..e56f99310 100755 --- a/src/Monitoring/ThreadWatcher.coffee +++ b/src/Monitoring/ThreadWatcher.coffee @@ -154,7 +154,7 @@ ThreadWatcher = unread = quotingYou = 0 - for postObj in @response.posts[1..] + for postObj in @response.posts continue unless postObj.no > lastReadPost continue if QR.db?.get {boardID, threadID, postID: postObj.no} unread++ diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee index 3c0de729f..b94b79a24 100755 --- a/src/Monitoring/Unread.coffee +++ b/src/Monitoring/Unread.coffee @@ -12,8 +12,10 @@ Unread = @db = new DataBoard 'lastReadPosts', @sync @hr = $.el 'hr', id: 'unread-line' - @posts = new RandomAccessList + @posts = new Set @postsQuotingYou = new Set + @order = new RandomAccessList + @position = null Thread.callbacks.push name: 'Unread' @@ -22,7 +24,26 @@ Unread = name: 'Unread' cb: @addPost - readCount: 0 + <% if (tests_enabled) { %> + testLink = $.el 'a', + textContent: 'Test Post Order' + $.on testLink, 'click', -> + list1 = (x.ID for x in Unread.order.order()) + list2 = (+x.id[2..] for x in $$ '.postContainer') + pass = do -> + return false unless list1.length is list2.length + for i in [0...list1.length] by 1 + return false if list1[i] isnt list2[i] + true + if pass + new Notice 'success', "Orders same (#{list1.length} posts)", 5 + else + new Notice 'warning', 'Orders differ.', 30 + c.log list1 + c.log list2 + Header.menu.addEntry + el: testLink + <% } %> node: -> Unread.thread = @ @@ -31,31 +52,33 @@ Unread = boardID: @board.ID threadID: @ID defaultValue: 0 - for ID in @posts.keys when +ID <= Unread.lastReadPost - Unread.readCount++ + Unread.readCount = 0 + Unread.readCount++ for ID in @posts.keys when +ID <= Unread.lastReadPost $.one d, '4chanXInitFinished', Unread.ready $.on d, 'ThreadUpdate', Unread.onUpdate $.on d, 'scroll visibilitychange', Unread.read - $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] and not Conf['Quote Threading'] + $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] ready: -> - if Conf['Quote Threading'] - QuoteThreading.force() - else - Unread.setLine true if Conf['Unread Line'] - Unread.read() - Unread.update() - Unread.scroll() if Conf['Scroll to Last Read Post'] and not Conf['Quote Threading'] + Unread.setLine true + Unread.read() + Unread.update() + Unread.scroll() if Conf['Scroll to Last Read Post'] + + positionPrev: -> + if Unread.position then Unread.position.prev else Unread.order.last scroll: -> # Let the header's onload callback handle it. return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts - # Scroll to the last displayed non-deleted read post. - {posts} = Unread.thread - for i in [Unread.readCount-1..0] by -1 - {root} = posts[posts.keys[i]].nodes - if root.getBoundingClientRect().height + position = Unread.positionPrev() + while position + {root} = position.data.nodes + if !root.getBoundingClientRect().height + # Don't try to scroll to posts with display: none + position = position.prev + else Header.scrollToIfNeeded root, true break return @@ -71,23 +94,27 @@ Unread = postIDs = Unread.thread.posts.keys for i in [Unread.readCount...postIDs.length] by 1 - ID = postIDs[i] - break if +ID > Unread.lastReadPost - Unread.posts.rm ID + ID = +postIDs[i] + break if ID > Unread.lastReadPost + Unread.posts.delete ID Unread.postsQuotingYou.delete ID Unread.readCount++ - Unread.setLine() if Conf['Unread Line'] and not Conf['Quote Threading'] + Unread.updatePosition() + Unread.setLine() Unread.update() addPost: -> - return if @isClone or @ID <= Unread.lastReadPost or !@isReply or @isHidden or QR.db?.get { + return if @isFetchedQuote or @isClone + Unread.order.push @ + return if @ID <= Unread.lastReadPost or @isHidden or QR.db?.get { boardID: @board.ID threadID: @thread.ID postID: @ID } - Unread.posts.push @ + Unread.posts.add @ID Unread.addPostQuotingYou @ + Unread.position ?= Unread.order[@ID] addPostQuotingYou: (post) -> for quotelink in post.nodes.quotelinks when QR.db?.get Get.postDataFromLink quotelink @@ -110,31 +137,31 @@ Unread = onUpdate: (e) -> if !e.detail[404] - # Force line on visible threads if there were no unread posts previously. - Unread.setLine(!Unread.hr.parentNode) if Conf['Unread Line'] and not Conf['Quote Threading'] + Unread.setLine() Unread.read() Unread.update() readSinglePost: (post) -> {ID} = post - {posts} = Unread - return unless posts[ID] - posts.rm ID + return unless Unread.posts.has ID + Unread.posts.delete ID Unread.postsQuotingYou.delete ID + Unread.updatePosition() Unread.saveLastReadPost() Unread.update() read: $.debounce 100, (e) -> - return if d.hidden or !Unread.posts.length + return if d.hidden or !Unread.posts.size height = doc.clientHeight - {posts} = Unread count = 0 - while post = posts.first - break unless Header.getBottomOf(post.data.nodes.root) > -1 # post is not completely read - {ID, data} = post + while Unread.position + {ID, data} = Unread.position + {root} = data.nodes + break unless !root.getBoundingClientRect().height or # post has been hidden + Header.getBottomOf(root) > -1 # post is completely read count++ - posts.rm ID + Unread.posts.delete ID Unread.postsQuotingYou.delete ID if Conf['Mark Quotes of You'] and QR.db?.get { @@ -142,18 +169,24 @@ Unread = threadID: data.thread.ID postID: ID } - QuoteYou.lastRead = data.nodes.root + QuoteYou.lastRead = root + Unread.position = Unread.position.next return unless count + Unread.updatePosition() Unread.saveLastReadPost() Unread.update() if e + updatePosition: -> + while Unread.position and !Unread.posts.has Unread.position.ID + Unread.position = Unread.position.next + saveLastReadPost: $.debounce 2 * $.SECOND, -> postIDs = Unread.thread.posts.keys for i in [Unread.readCount...postIDs.length] by 1 - ID = postIDs[i] - break if Unread.posts[ID] - Unread.lastReadPost = +ID + ID = +postIDs[i] + break if Unread.posts.has ID + Unread.lastReadPost = ID Unread.readCount++ return if Unread.thread.isDead and !Unread.thread.isArchived Unread.db.forceSync() @@ -163,15 +196,16 @@ Unread = val: Unread.lastReadPost setLine: (force) -> - return unless d.hidden or force is true - return $.rm Unread.hr unless Unread.posts.length - {posts} = Unread.thread - for i in [Unread.readCount-1..0] by -1 - return $.after posts[posts.keys[i]].nodes.root, Unread.hr - return + return unless Conf['Unread Line'] + if d.hidden or (force is true) + if Unread.linePosition = Unread.positionPrev() + $.after Unread.linePosition.data.nodes.root, Unread.hr + else + $.rm Unread.hr + Unread.hr.hidden = Unread.linePosition is Unread.order.last update: -> - count = Unread.posts.length + count = Unread.posts.size countQuotingYou = Unread.postsQuotingYou.size if Conf['Unread Count'] diff --git a/src/Quotelinks/QuoteThreading.coffee b/src/Quotelinks/QuoteThreading.coffee index c11d4ebdd..106232081 100755 --- a/src/Quotelinks/QuoteThreading.coffee +++ b/src/Quotelinks/QuoteThreading.coffee @@ -9,96 +9,116 @@ QuoteThreading = @enabled = true @controls = $.el 'span', <%= html('') %> + @threadNewLink = $.el 'span', + className: 'brackets-wrap threadnewlink' + hidden: true + $.extend @threadNewLink, <%= html('Thread New Posts') %> - input = $ 'input', @controls - $.on input, 'change', @toggle + $.on $('input', @controls), 'change', -> + QuoteThreading.rethread @checked + $.on @threadNewLink.firstElementChild, 'click', -> + QuoteThreading.threadNewLink.hidden = true + QuoteThreading.rethread true Header.menu.addEntry @entry = el: @controls order: 98 + Thread.callbacks.push + name: 'Quote Threading' + cb: @setThread Post.callbacks.push name: 'Quote Threading' cb: @node - force: -> - g.posts.forEach (post) -> - post.cb true if post.cb - Unread.read() - Unread.update() + parent: {} + children: {} + inserted: {} + + setThread: -> + QuoteThreading.thread = @ + $.asap (-> !Conf['Thread Updater'] or $ '.navLinksBot > .updatelink'), -> + $.add $('.navLinksBot'), [$.tn(' '), QuoteThreading.threadNewLink] node: -> - {posts} = g - return if @isClone or not QuoteThreading.enabled or !@isReply or @isHidden + return if @isFetchedQuote or @isClone or !@isReply + {thread} = QuoteThreading + parents = for quote in @quotes + parent = g.posts[quote] + continue if !parent or parent.isFetchedQuote or !parent.isReply or parent.ID >= @ID + parent + if parents.length is 1 + QuoteThreading.parent[@fullID] = parents[0] - keys = [] - len = g.BOARD.ID.length + 1 - keys.push quote for quote in @quotes when (quote[len..] < @ID) and quote of posts + descendants: (post) -> + posts = [post] + if children = QuoteThreading.children[post.fullID] + for child in children + posts = posts.concat QuoteThreading.descendants child + posts - return unless keys.length is 1 + insert: (post) -> + return false unless QuoteThreading.enabled and + (parent = QuoteThreading.parent[post.fullID]) and + !QuoteThreading.inserted[post.fullID] - @threaded = keys[0] - @cb = QuoteThreading.nodeinsert + descendants = QuoteThreading.descendants post + if !Unread.posts.has(parent.ID) and descendants.some((x) -> Unread.posts.has(x.ID)) + QuoteThreading.threadNewLink.hidden = false + return false - nodeinsert: (force) -> - post = g.posts[@threaded] - - return false if @thread.OP is post - - {posts} = Unread - {root} = post.nodes - - unless force - height = doc.clientHeight - {bottom, top} = root.getBoundingClientRect() - - # Post is unread or is fully visible. - return false unless posts[post.ID] or ((bottom < height) and (top > 0)) - - if $.hasClass root, 'threadOP' - threadContainer = root.nextElementSibling - post = Get.postFromRoot $.x 'descendant::div[contains(@class,"postContainer")][last()]', threadContainer - $.add threadContainer, @nodes.root + {order} = Unread + children = (QuoteThreading.children[parent.fullID] or= []) + threadContainer = parent.nodes.threadContainer or $.el 'div', className: 'threadContainer' + nodes = [post.nodes.root] + nodes.push post.nodes.threadContainer if post.nodes.threadContainer + i = children.length + i-- for child in children by -1 when child.ID >= post.ID + if i isnt children.length + next = children[i] + order.before order[next.ID], order[x.ID] for x in descendants + children.splice i, 0, post + $.before next.nodes.root, nodes else - threadContainer = $.el 'div', - className: 'threadContainer' - $.add threadContainer, @nodes.root - $.after root, threadContainer - $.addClass root, 'threadOP' + prev = parent + while (prev2 = QuoteThreading.children[prev.fullID]) and prev2.length + prev = prev2[prev2.length-1] + order.after order[prev.ID], order[x.ID] for x in descendants by -1 + children.push post + $.add threadContainer, nodes - if post = posts[post.ID] - posts.after post, posts[@ID] + QuoteThreading.inserted[post.fullID] = true - else if posts[@ID] - posts.prepend posts[@ID] + unless parent.nodes.threadContainer + parent.nodes.threadContainer = threadContainer + $.addClass parent.nodes.root, 'threadOP' + $.after parent.nodes.root, threadContainer return true - toggle: -> - if QuoteThreading.enabled = @checked - QuoteThreading.force() + rethread: (enabled) -> + {thread} = QuoteThreading + {posts} = thread + if QuoteThreading.enabled = enabled + posts.forEach QuoteThreading.insert else - thread = $('.thread') - posts = [] nodes = [] - - g.posts.forEach (post) -> - posts.push post unless post is post.thread.OP or post.isClone + Unread.order = new RandomAccessList + QuoteThreading.inserted = {} + posts.forEach (post) -> + Unread.order.push post + nodes.push post.nodes.root if post.isReply + if QuoteThreading.children[post.fullID] + delete QuoteThreading.children[post.fullID] + $.rmClass post.nodes.root, 'threadOP' + $.rm post.nodes.threadContainer + delete post.nodes.threadContainer + $.add thread.OP.nodes.root.parentNode, nodes - posts.sort (a, b) -> a.ID - b.ID - - nodes.push post.nodes.root for post in posts - $.add thread, nodes - - containers = $$ '.threadContainer', thread - $.rm container for container in containers - $.rmClass post, 'threadOP' for post in $$ '.threadOP' - - return - - kb: -> - control = $.id 'threadingControl' - control.checked = not control.checked - QuoteThreading.toggle.call control + Unread.position = Unread.order.first + Unread.updatePosition() + Unread.setLine true + Unread.read() + Unread.update()