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()