Overhaul quote threading.

- Separate post order data from read/unread status.
- Make compatible with unread line and scroll to last read post.
- Implement [Thread New Posts] link.
This commit is contained in:
ccd0 2014-11-30 12:46:07 -08:00
parent 748258f59b
commit c38c37c720
5 changed files with 169 additions and 118 deletions

View File

@ -197,6 +197,7 @@ Main =
Main.callbackNodes Thread, threads Main.callbackNodes Thread, threads
Main.callbackNodesDB Post, posts, -> Main.callbackNodesDB Post, posts, ->
QuoteThreading.insert post for post in posts
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
else else

View File

@ -320,12 +320,8 @@ ThreadUpdater =
ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25 ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
for post in posts for post in posts
root = post.nodes.root unless QuoteThreading.insert post
if post.cb $.add ThreadUpdater.root, post.nodes.root
unless post.cb()
$.add ThreadUpdater.root, root
else
$.add ThreadUpdater.root, root
$.event 'PostsInserted' $.event 'PostsInserted'
if scroll if scroll

View File

@ -154,7 +154,7 @@ ThreadWatcher =
unread = quotingYou = 0 unread = quotingYou = 0
for postObj in @response.posts[1..] for postObj in @response.posts
continue unless postObj.no > lastReadPost continue unless postObj.no > lastReadPost
continue if QR.db?.get {boardID, threadID, postID: postObj.no} continue if QR.db?.get {boardID, threadID, postID: postObj.no}
unread++ unread++

View File

@ -12,8 +12,10 @@ Unread =
@db = new DataBoard 'lastReadPosts', @sync @db = new DataBoard 'lastReadPosts', @sync
@hr = $.el 'hr', @hr = $.el 'hr',
id: 'unread-line' id: 'unread-line'
@posts = new RandomAccessList @posts = new Set
@postsQuotingYou = new Set @postsQuotingYou = new Set
@order = new RandomAccessList
@position = null
Thread.callbacks.push Thread.callbacks.push
name: 'Unread' name: 'Unread'
@ -22,7 +24,26 @@ Unread =
name: 'Unread' name: 'Unread'
cb: @addPost 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: -> node: ->
Unread.thread = @ Unread.thread = @
@ -31,31 +52,33 @@ Unread =
boardID: @board.ID boardID: @board.ID
threadID: @ID threadID: @ID
defaultValue: 0 defaultValue: 0
for ID in @posts.keys when +ID <= Unread.lastReadPost Unread.readCount = 0
Unread.readCount++ Unread.readCount++ for ID in @posts.keys when +ID <= Unread.lastReadPost
$.one d, '4chanXInitFinished', Unread.ready $.one d, '4chanXInitFinished', Unread.ready
$.on d, 'ThreadUpdate', Unread.onUpdate $.on d, 'ThreadUpdate', Unread.onUpdate
$.on d, 'scroll visibilitychange', Unread.read $.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: -> ready: ->
if Conf['Quote Threading'] Unread.setLine true
QuoteThreading.force() Unread.read()
else Unread.update()
Unread.setLine true if Conf['Unread Line'] Unread.scroll() if Conf['Scroll to Last Read Post']
Unread.read()
Unread.update() positionPrev: ->
Unread.scroll() if Conf['Scroll to Last Read Post'] and not Conf['Quote Threading'] if Unread.position then Unread.position.prev else Unread.order.last
scroll: -> scroll: ->
# Let the header's onload callback handle it. # Let the header's onload callback handle it.
return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts
# Scroll to the last displayed non-deleted read post. position = Unread.positionPrev()
{posts} = Unread.thread while position
for i in [Unread.readCount-1..0] by -1 {root} = position.data.nodes
{root} = posts[posts.keys[i]].nodes if !root.getBoundingClientRect().height
if root.getBoundingClientRect().height # Don't try to scroll to posts with display: none
position = position.prev
else
Header.scrollToIfNeeded root, true Header.scrollToIfNeeded root, true
break break
return return
@ -71,23 +94,27 @@ Unread =
postIDs = Unread.thread.posts.keys postIDs = Unread.thread.posts.keys
for i in [Unread.readCount...postIDs.length] by 1 for i in [Unread.readCount...postIDs.length] by 1
ID = postIDs[i] ID = +postIDs[i]
break if +ID > Unread.lastReadPost break if ID > Unread.lastReadPost
Unread.posts.rm ID Unread.posts.delete ID
Unread.postsQuotingYou.delete ID Unread.postsQuotingYou.delete ID
Unread.readCount++ Unread.readCount++
Unread.setLine() if Conf['Unread Line'] and not Conf['Quote Threading'] Unread.updatePosition()
Unread.setLine()
Unread.update() Unread.update()
addPost: -> 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 boardID: @board.ID
threadID: @thread.ID threadID: @thread.ID
postID: @ID postID: @ID
} }
Unread.posts.push @ Unread.posts.add @ID
Unread.addPostQuotingYou @ Unread.addPostQuotingYou @
Unread.position ?= Unread.order[@ID]
addPostQuotingYou: (post) -> addPostQuotingYou: (post) ->
for quotelink in post.nodes.quotelinks when QR.db?.get Get.postDataFromLink quotelink for quotelink in post.nodes.quotelinks when QR.db?.get Get.postDataFromLink quotelink
@ -110,31 +137,31 @@ Unread =
onUpdate: (e) -> onUpdate: (e) ->
if !e.detail[404] if !e.detail[404]
# Force line on visible threads if there were no unread posts previously. Unread.setLine()
Unread.setLine(!Unread.hr.parentNode) if Conf['Unread Line'] and not Conf['Quote Threading']
Unread.read() Unread.read()
Unread.update() Unread.update()
readSinglePost: (post) -> readSinglePost: (post) ->
{ID} = post {ID} = post
{posts} = Unread return unless Unread.posts.has ID
return unless posts[ID] Unread.posts.delete ID
posts.rm ID
Unread.postsQuotingYou.delete ID Unread.postsQuotingYou.delete ID
Unread.updatePosition()
Unread.saveLastReadPost() Unread.saveLastReadPost()
Unread.update() Unread.update()
read: $.debounce 100, (e) -> read: $.debounce 100, (e) ->
return if d.hidden or !Unread.posts.length return if d.hidden or !Unread.posts.size
height = doc.clientHeight height = doc.clientHeight
{posts} = Unread
count = 0 count = 0
while post = posts.first while Unread.position
break unless Header.getBottomOf(post.data.nodes.root) > -1 # post is not completely read {ID, data} = Unread.position
{ID, data} = post {root} = data.nodes
break unless !root.getBoundingClientRect().height or # post has been hidden
Header.getBottomOf(root) > -1 # post is completely read
count++ count++
posts.rm ID Unread.posts.delete ID
Unread.postsQuotingYou.delete ID Unread.postsQuotingYou.delete ID
if Conf['Mark Quotes of You'] and QR.db?.get { if Conf['Mark Quotes of You'] and QR.db?.get {
@ -142,18 +169,24 @@ Unread =
threadID: data.thread.ID threadID: data.thread.ID
postID: ID postID: ID
} }
QuoteYou.lastRead = data.nodes.root QuoteYou.lastRead = root
Unread.position = Unread.position.next
return unless count return unless count
Unread.updatePosition()
Unread.saveLastReadPost() Unread.saveLastReadPost()
Unread.update() if e Unread.update() if e
updatePosition: ->
while Unread.position and !Unread.posts.has Unread.position.ID
Unread.position = Unread.position.next
saveLastReadPost: $.debounce 2 * $.SECOND, -> saveLastReadPost: $.debounce 2 * $.SECOND, ->
postIDs = Unread.thread.posts.keys postIDs = Unread.thread.posts.keys
for i in [Unread.readCount...postIDs.length] by 1 for i in [Unread.readCount...postIDs.length] by 1
ID = postIDs[i] ID = +postIDs[i]
break if Unread.posts[ID] break if Unread.posts.has ID
Unread.lastReadPost = +ID Unread.lastReadPost = ID
Unread.readCount++ Unread.readCount++
return if Unread.thread.isDead and !Unread.thread.isArchived return if Unread.thread.isDead and !Unread.thread.isArchived
Unread.db.forceSync() Unread.db.forceSync()
@ -163,15 +196,16 @@ Unread =
val: Unread.lastReadPost val: Unread.lastReadPost
setLine: (force) -> setLine: (force) ->
return unless d.hidden or force is true return unless Conf['Unread Line']
return $.rm Unread.hr unless Unread.posts.length if d.hidden or (force is true)
{posts} = Unread.thread if Unread.linePosition = Unread.positionPrev()
for i in [Unread.readCount-1..0] by -1 $.after Unread.linePosition.data.nodes.root, Unread.hr
return $.after posts[posts.keys[i]].nodes.root, Unread.hr else
return $.rm Unread.hr
Unread.hr.hidden = Unread.linePosition is Unread.order.last
update: -> update: ->
count = Unread.posts.length count = Unread.posts.size
countQuotingYou = Unread.postsQuotingYou.size countQuotingYou = Unread.postsQuotingYou.size
if Conf['Unread Count'] if Conf['Unread Count']

View File

@ -9,96 +9,116 @@ QuoteThreading =
@enabled = true @enabled = true
@controls = $.el 'span', @controls = $.el 'span',
<%= html('<label><input id="threadingControl" type="checkbox" checked> Threading</label>') %> <%= html('<label><input id="threadingControl" type="checkbox" checked> Threading</label>') %>
@threadNewLink = $.el 'span',
className: 'brackets-wrap threadnewlink'
hidden: true
$.extend @threadNewLink, <%= html('<a href="javascript:;">Thread New Posts</a>') %>
input = $ 'input', @controls $.on $('input', @controls), 'change', ->
$.on input, 'change', @toggle QuoteThreading.rethread @checked
$.on @threadNewLink.firstElementChild, 'click', ->
QuoteThreading.threadNewLink.hidden = true
QuoteThreading.rethread true
Header.menu.addEntry @entry = Header.menu.addEntry @entry =
el: @controls el: @controls
order: 98 order: 98
Thread.callbacks.push
name: 'Quote Threading'
cb: @setThread
Post.callbacks.push Post.callbacks.push
name: 'Quote Threading' name: 'Quote Threading'
cb: @node cb: @node
force: -> parent: {}
g.posts.forEach (post) -> children: {}
post.cb true if post.cb inserted: {}
Unread.read()
Unread.update() setThread: ->
QuoteThreading.thread = @
$.asap (-> !Conf['Thread Updater'] or $ '.navLinksBot > .updatelink'), ->
$.add $('.navLinksBot'), [$.tn(' '), QuoteThreading.threadNewLink]
node: -> node: ->
{posts} = g return if @isFetchedQuote or @isClone or !@isReply
return if @isClone or not QuoteThreading.enabled or !@isReply or @isHidden {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 = [] descendants: (post) ->
len = g.BOARD.ID.length + 1 posts = [post]
keys.push quote for quote in @quotes when (quote[len..] < @ID) and quote of posts 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] descendants = QuoteThreading.descendants post
@cb = QuoteThreading.nodeinsert if !Unread.posts.has(parent.ID) and descendants.some((x) -> Unread.posts.has(x.ID))
QuoteThreading.threadNewLink.hidden = false
return false
nodeinsert: (force) -> {order} = Unread
post = g.posts[@threaded] children = (QuoteThreading.children[parent.fullID] or= [])
threadContainer = parent.nodes.threadContainer or $.el 'div', className: 'threadContainer'
return false if @thread.OP is post nodes = [post.nodes.root]
nodes.push post.nodes.threadContainer if post.nodes.threadContainer
{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
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 else
threadContainer = $.el 'div', prev = parent
className: 'threadContainer' while (prev2 = QuoteThreading.children[prev.fullID]) and prev2.length
$.add threadContainer, @nodes.root prev = prev2[prev2.length-1]
$.after root, threadContainer order.after order[prev.ID], order[x.ID] for x in descendants by -1
$.addClass root, 'threadOP' children.push post
$.add threadContainer, nodes
if post = posts[post.ID] QuoteThreading.inserted[post.fullID] = true
posts.after post, posts[@ID]
else if posts[@ID] unless parent.nodes.threadContainer
posts.prepend posts[@ID] parent.nodes.threadContainer = threadContainer
$.addClass parent.nodes.root, 'threadOP'
$.after parent.nodes.root, threadContainer
return true return true
toggle: -> rethread: (enabled) ->
if QuoteThreading.enabled = @checked {thread} = QuoteThreading
QuoteThreading.force() {posts} = thread
if QuoteThreading.enabled = enabled
posts.forEach QuoteThreading.insert
else else
thread = $('.thread')
posts = []
nodes = [] nodes = []
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
g.posts.forEach (post) -> Unread.position = Unread.order.first
posts.push post unless post is post.thread.OP or post.isClone Unread.updatePosition()
Unread.setLine true
posts.sort (a, b) -> a.ID - b.ID Unread.read()
Unread.update()
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