Merge branch 'v3'

This commit is contained in:
Zixaphir 2014-01-17 13:23:36 -07:00
commit cc7decfd34
24 changed files with 1294 additions and 896 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,6 @@ Recursive =
apply: (recursive, post, args...) -> apply: (recursive, post, args...) ->
{fullID} = post {fullID} = post
for ID, post of g.posts g.posts.forEach (post) ->
if fullID in post.quotes if fullID in post.quotes
recursive post, args... recursive post, args...
return

View File

@ -40,22 +40,28 @@ Get =
allQuotelinksLinkingTo: (post) -> allQuotelinksLinkingTo: (post) ->
# Get quotelinks & backlinks linking to the given post. # Get quotelinks & backlinks linking to the given post.
quotelinks = [] quotelinks = []
handleQuotes = (post, type) -> {posts} = g
quotelinks.push post.nodes[type]... fullID = {post}
quotelinks.push clone.nodes[type]... for clone in post.clones handleQuotes = (qPost, type) ->
quotelinks.push qPost.nodes[type]...
quotelinks.push clone.nodes[type]... for clone in qPost.clones
return return
# First: # First:
# In every posts, # In every posts,
# if it did quote this post, # if it did quote this post,
# get all their backlinks. # get all their backlinks.
handleQuotes quoterPost, 'quotelinks' for ID, quoterPost of g.posts when post.fullID in quoterPost.quotes posts.forEach (qPost) ->
if fullID in qPost.quotes
handleQuotes qPost, 'quotelinks'
# Second: # Second:
# If we have quote backlinks: # If we have quote backlinks:
# in all posts this post quoted # in all posts this post quoted
# and their clones, # and their clones,
# get all of their backlinks. # get all of their backlinks.
if Conf['Quote Backlinks'] if Conf['Quote Backlinks']
handleQuotes quotedPost, 'backlinks' for quote in post.quotes when quotedPost = g.posts[quote] handleQuotes qPost, 'backlinks' for quote in post.quotes when qPost = posts[quote]
# Third: # Third:
# Filter out irrelevant quotelinks. # Filter out irrelevant quotelinks.
quotelinks.filter (quotelink) -> quotelinks.filter (quotelink) ->

View File

@ -9,10 +9,7 @@ doc = d.documentElement
g = g =
VERSION: '<%= version %>' VERSION: '<%= version %>'
NAMESPACE: '<%= meta.name.replace(' ', '_') %>.' NAMESPACE: '<%= meta.name.replace(' ', '_') %>.'
TYPE: 'sfw'
boards: {} boards: {}
threads: {}
posts: {}
Mascots = Mascots =
'Akiyama_Mio': 'Akiyama_Mio':

View File

@ -2,6 +2,8 @@ Index =
init: -> init: ->
return if g.BOARD.ID is 'f' or g.VIEW is 'catalog' or !Conf['JSON Navigation'] return if g.BOARD.ID is 'f' or g.VIEW is 'catalog' or !Conf['JSON Navigation']
@board = "#{g.BOARD}"
@button = $.el 'a', @button = $.el 'a',
className: 'index-refresh-shortcut fa' className: 'index-refresh-shortcut fa'
title: 'Refresh Index' title: 'Refresh Index'
@ -113,12 +115,13 @@ Index =
scroll: $.debounce 100, -> scroll: $.debounce 100, ->
return if Index.req or Conf['Index Mode'] isnt 'infinite' or (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread' return if Index.req or Conf['Index Mode'] isnt 'infinite' or (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread'
Index.pageNum = Index.getCurrentPage() unless Index.pageNum? # Avoid having to pushState to keep track of the current page Index.pageNum = Index.getCurrentPage() unless Index.pageNum? # Avoid having to pushState to keep track of the current page
pageNum = Index.pageNum++ pageNum = Index.pageNum++
return Index.endNotice() if pageNum >= Index.pagesNum return Index.endNotice() if pageNum >= Index.pagesNum
nodesPerPage = Index.threadsNumPerPage * 2
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)] nodes = Index.buildSinglePage pageNum
Index.buildReplies nodes if Conf['Show Replies'] Index.buildReplies nodes if Conf['Show Replies']
$.add Index.root, nodes Index.buildStructure nodes
Index.setPage pageNum Index.setPage pageNum
endNotice: do -> endNotice: do ->
@ -240,7 +243,7 @@ Index =
onabort: onload onabort: onload
onloadend: onload onloadend: onload
, ,
whenModified: true whenModified: Index.board is "#{g.BOARD}"
$.addClass Index.button, 'fa-spin' $.addClass Index.button, 'fa-spin'
load: (e, pageNum) -> load: (e, pageNum) ->
@ -266,13 +269,16 @@ Index =
new Notice 'warning', err, 1 new Notice 'warning', err, 1
return return
Navigate.title()
Index.board = "#{g.BOARD}"
try try
if req.status is 200 if req.status is 200
Index.parse JSON.parse(req.response), pageNum Index.parse JSON.parse(req.response), pageNum
else if req.status is 304 and pageNum? else if req.status is 304 and pageNum?
Index.pageNav pageNum Index.pageNav pageNum
catch err catch err
c.error 'Index failure:', err c.error "Index failure: #{err.message}", err.stack
# network error or non-JSON content for example. # network error or non-JSON content for example.
if notice if notice
notice.setType 'error' notice.setType 'error'
@ -303,8 +309,8 @@ Index =
Index.threadsNumPerPage = pages[0].threads.length Index.threadsNumPerPage = pages[0].threads.length
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), [] Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
for threadID, thread of g.BOARD.threads when thread.ID not in Index.liveThreadIDs g.BOARD.threads.forEach (thread) ->
thread.collect() thread.collect() unless thread.ID in Index.liveThreadIDs
return return
buildThreads: -> buildThreads: ->
@ -312,23 +318,23 @@ Index =
threads = [] threads = []
posts = [] posts = []
for threadData, i in Index.liveThreadData for threadData, i in Index.liveThreadData
threadRoot = Build.thread g.BOARD, threadData
Index.nodes.push threadRoot, $.el 'hr'
if thread = g.BOARD.threads[threadData.no]
thread.setPage Math.floor i / Index.threadsNumPerPage
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
else
thread = new Thread threadData.no, g.BOARD
threads.push thread
continue if thread.ID of thread.posts
try try
threadRoot = Build.thread g.BOARD, threadData
if thread = g.BOARD.threads[threadData.no]
thread.setPage Math.floor i / Index.threadsNumPerPage
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
else
thread = new Thread threadData.no, g.BOARD
threads.push thread
Index.nodes.push threadRoot
continue if thread.ID of thread.posts
posts.push new Post $('.opContainer', threadRoot), thread, g.BOARD posts.push new Post $('.opContainer', threadRoot), thread, g.BOARD
catch err catch err
# Skip posts that we failed to parse. # Skip posts that we failed to parse.
errors = [] unless errors errors = [] unless errors
errors.push errors.push
message: "Parsing of Post No.#{thread} failed. Post will be skipped." message: "Parsing of Thread No.#{thread} failed. Thread will be skipped."
error: err error: err
Main.handleErrors errors if errors Main.handleErrors errors if errors
@ -340,7 +346,7 @@ Index =
buildReplies: (threadRoots) -> buildReplies: (threadRoots) ->
posts = [] posts = []
for threadRoot in threadRoots by 2 for threadRoot in threadRoots
thread = Get.threadFromRoot threadRoot thread = Get.threadFromRoot threadRoot
i = Index.liveThreadIDs.indexOf thread.ID i = Index.liveThreadIDs.indexOf thread.ID
continue unless lastReplies = Index.liveThreadData[i].last_replies continue unless lastReplies = Index.liveThreadData[i].last_replies
@ -364,52 +370,89 @@ Index =
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
sort: -> sort: ->
switch Conf['Index Sort'] {liveThreadIDs, liveThreadData} = Index
when 'bump' sortedThreadIDs = {
sortedThreadIDs = Index.liveThreadIDs lastreply:
when 'lastreply' [liveThreadData...].sort((a, b) ->
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> a = num[num.length - 1] if (num = a.last_replies)
a = a.last_replies[a.last_replies.length - 1] if 'last_replies' of a b = num[num.length - 1] if (num = b.last_replies)
b = b.last_replies[b.last_replies.length - 1] if 'last_replies' of b
b.no - a.no b.no - a.no
).map (data) -> data.no ).map (post) -> post.no
when 'birth' bump: liveThreadIDs
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a birth: [liveThreadIDs... ].sort (a, b) -> b - a
when 'replycount' replycount: [liveThreadData...].sort((a, b) -> b.replies - a.replies).map (post) -> post.no
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.replies - a.replies).map (data) -> data.no filecount: [liveThreadData...].sort((a, b) -> b.images - a.images ).map (post) -> post.no
when 'filecount' }[Conf['Index Sort']]
sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.images - a.images).map (data) -> data.no Index.sortedNodes = sortedNodes = new RandomAccessList
Index.sortedNodes = [] {nodes} = Index
for threadID in sortedThreadIDs for threadID in sortedThreadIDs
i = Index.liveThreadIDs.indexOf(threadID) * 2 sortedNodes.push nodes[Index.liveThreadIDs.indexOf(threadID)]
Index.sortedNodes.push Index.nodes[i], Index.nodes[i + 1] if Index.isSearching and nodes = Index.querySearch(Index.searchInput.value)
if Index.isSearching Index.sortedNodes = new RandomAccessList nodes
Index.sortedNodes = Index.querySearch(Index.searchInput.value) or Index.sortedNodes items = [
# Sticky threads # Sticky threads
Index.sortOnTop (thread) -> thread.isSticky fn: (thread) -> thread.isSticky
# Highlighted threads cnd: true
Index.sortOnTop((thread) -> thread.isOnTop) if Conf['Filter'] , # Highlighted threads
# Non-hidden threads fn: (thread) -> thread.isOnTop
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads'] cnd: Conf['Filter']
, # Non-hidden threads
fn: (thread) -> !thread.isHidden
cnd: Conf['Anchor Hidden Threads']
]
i = 0
while item = items[i++]
{fn, cnd} = item
Index.sortOnTop fn if cnd
return
sortOnTop: (match) -> sortOnTop: (match) ->
offset = 0 offset = 0
for threadRoot, i in Index.sortedNodes by 2 when match Get.threadFromRoot threadRoot {sortedNodes} = Index
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)... threadRoot = sortedNodes.first
while threadRoot
if match Get.threadFromRoot threadRoot.data
target = sortedNodes.first
j = 0
while j++ < offset
target = target.next
unless threadRoot is target
offset++
sortedNodes.before target, threadRoot
threadRoot = threadRoot.next
return return
buildIndex: -> buildIndex: ->
if Conf['Index Mode'] isnt 'all pages' if Conf['Index Mode'] isnt 'all pages'
pageNum = Index.getCurrentPage() nodes = Index.buildSinglePage Index.getCurrentPage()
nodesPerPage = Index.threadsNumPerPage * 2
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
else else
nodes = Index.sortedNodes nodes = [(target = Index.sortedNodes.first).data]
while target = target.next
nodes.push target.data
$.rmAll Index.root $.rmAll Index.root
$.rmAll Header.hover $.rmAll Header.hover
Index.buildReplies nodes if Conf['Show Replies'] Index.buildReplies nodes if Conf['Show Replies']
$.add Index.root, nodes Index.buildStructure nodes
$.event 'IndexBuild', nodes
buildSinglePage: (pageNum) ->
nodes = []
nodesPerPage = Index.threadsNumPerPage
offset = nodesPerPage * pageNum
end = offset + nodesPerPage
target = Index.sortedNodes.order()[offset]
Index.sortedNodes
while (offset++ <= end) and target
nodes.push target.data
target = target.next
nodes
buildStructure: (nodes) ->
result = $.frag()
i = 0
$.add result, [node, $.el 'hr'] while node = nodes[i++]
$.add Index.root, result
$.rm hr for hr in $$ 'hr + hr', Index.root # Temp fix until I figure out where I fucked up
$.event 'IndexBuild', result
isSearching: false isSearching: false
@ -449,11 +492,14 @@ Index =
return unless keywords = query.toLowerCase().match /\S+/g return unless keywords = query.toLowerCase().match /\S+/g
Index.search keywords Index.search keywords
search: (keywords) -> search: (keywords) ->
found = [] found = []
for threadRoot, i in Index.sortedNodes by 2 target = Index.sortedNodes.first
if Index.searchMatch Get.threadFromRoot(threadRoot), keywords while target
found.push Index.sortedNodes[i], Index.sortedNodes[i + 1] {data} = target
if Index.searchMatch Get.threadFromRoot(data), keywords
found.push data
target = target.next
found found
searchMatch: (thread, keywords) -> searchMatch: (thread, keywords) ->

View File

@ -1,5 +1,8 @@
Main = Main =
init: -> init: ->
g.threads = new SimpleDict
g.posts = new SimpleDict
pathname = location.pathname.split '/' pathname = location.pathname.split '/'
g.BOARD = new Board pathname[1] g.BOARD = new Board pathname[1]
return if g.BOARD.ID in ['z', 'fk'] return if g.BOARD.ID in ['z', 'fk']
@ -122,7 +125,7 @@ Main =
'left=0,top=0,width=500,height=255,toolbar=0,resizable=0' 'left=0,top=0,width=500,height=255,toolbar=0,resizable=0'
$.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0'] $.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0']
if g.VIEW is 'thread' or !Conf['JSON Navigation'] unless Conf['JSON Navigation'] and g.VIEW is 'index'
Main.initThread() Main.initThread()
else else
$.event '4chanXInitFinished' $.event '4chanXInitFinished'

View File

@ -1,11 +1,13 @@
Navigate = Navigate =
path: window.location.pathname path: window.location.pathname
init: -> init: ->
return if g.VIEW is 'catalog' or g.BOARD.ID is 'f' or !Conf['JSON Navigation'] return if g.VIEW is 'catalog' or g.BOARD.ID is 'f' or !Conf['JSON Navigation']
# blink/webkit throw a popstate on page load. Not what we want. # blink/webkit throw a popstate on page load. Not what we want.
$.ready -> $.on window, 'popstate', Navigate.popstate $.ready -> $.on window, 'popstate', Navigate.popstate
@title = -> return
Thread.callbacks.push Thread.callbacks.push
name: 'Navigate' name: 'Navigate'
cb: @thread cb: @thread
@ -34,10 +36,7 @@ Navigate =
{posts, threads} = g {posts, threads} = g
# Garbage collection # Garbage collection
g.posts = {} g.threads.forEach (thread) -> thread.collect()
g.threads = {}
g.BOARD.posts = {}
g.BOARD.threads = {}
QuoteBacklink.containers = {} QuoteBacklink.containers = {}
@ -84,7 +83,7 @@ Navigate =
feature() if condition feature() if condition
catch err catch err
error = [ error = [
message: "Quote Threading Failed." message: "#{name} Failed."
error: err error: err
] ]
Main.handleErrors error if error Main.handleErrors error if error
@ -106,10 +105,13 @@ Navigate =
$.off d, 'IndexRefresh', QR.generatePostableThreadsList $.off d, 'IndexRefresh', QR.generatePostableThreadsList
updateBoard: (boardID) -> updateBoard: (boardID) ->
g.BOARD = new Board boardID
req = null req = null
fullBoardList = $ '#full-board-list', Header.boardList
$.rmClass $('.current', fullBoardList), 'current'
$.addClass $("a[href*='/#{boardID}/']", fullBoardList), 'current'
Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
onload = (e) -> onload = (e) ->
if e.type is 'abort' if e.type is 'abort'
req.onloadend = null req.onloadend = null
@ -117,9 +119,10 @@ Navigate =
return unless req.status is 200 return unless req.status is 200
board = do -> try try
for board in JSON.parse(req.response).boards for aboard in JSON.parse(req.response).boards when aboard.board is boardID
return board if board.board is boardID board = aboard
break
catch err catch err
Main.handleErrors [ Main.handleErrors [
@ -130,33 +133,36 @@ Navigate =
return unless board return unless board
Navigate.updateTitle board Navigate.updateTitle board
Navigate.updateFavicon !!board.ws_board
return if Favicon.SFW is sfw = !!board.ws_board # Board SFW status hasn't changed
g.TYPE = if sfw then 'sfw' else 'nsfw'
if Conf["NSFW/SFW Mascots"]
Main.setMascotString()
MascotTools.toggle()
if Conf["NSFW/SFW Themes"]
Main.setThemeString()
theme = Themes[Conf[g.THEMESTRING] or if sfw then 'Yotsuba B' else 'Yotsuba'] or Themes[Conf[g.THEMESTRING] = if sfw then 'Yotsuba B' else 'Yotsuba']
Style.setTheme theme
Favicon.SFW = sfw
Favicon.el.href = "//s.4cdn.org/image/favicon#{if sfw then '-ws' else ''}.ico"
$.add d.head, Favicon.el # Changing the href alone doesn't update the icon on Firefox
Favicon.init()
fullBoardList = $ '#full-board-list', Header.boardList
$.rmClass $('.current', fullBoardList), 'current'
$.addClass $("a[href*='/#{boardID}/']", fullBoardList), 'current'
Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
req = $.ajax '//a.4cdn.org/boards.json', req = $.ajax '//a.4cdn.org/boards.json',
onabort: onload onabort: onload
onloadend: onload onloadend: onload
updateFavicon: (sfw) ->
# TODO: think of a better name for this. Changes style, too.
Favicon.el.href = "//s.4cdn.org/image/favicon#{if sfw then '-ws' else ''}.ico"
$.add d.head, Favicon.el # Changing the href alone doesn't update the icon on Firefox
return if Favicon.SFW is sfw # Board SFW status hasn't changed
Favicon.SFW = sfw
Favicon.update()
g.TYPE = if sfw then 'sfw' else 'nsfw'
if Conf["NSFW/SFW Mascots"]
Main.setMascotString()
MascotTools.toggle()
if Conf["NSFW/SFW Themes"]
Main.setThemeString()
theme = Themes[Conf[g.THEMESTRING] or if sfw then 'Yotsuba B' else 'Yotsuba'] or Themes[Conf[g.THEMESTRING] = if sfw then 'Yotsuba B' else 'Yotsuba']
Style.setTheme theme
mainStyleSheet.href = newStyleSheet.href
Main.setClass()
updateTitle: ({board, title}) -> updateTitle: ({board, title}) ->
$.rm subtitle if subtitle = $ '.boardSubtitle' $.rm subtitle if subtitle = $ '.boardSubtitle'
$('.boardTitle').textContent = d.title = "/#{board}/ - #{title}" $('.boardTitle').textContent = d.title = "/#{board}/ - #{title}"
@ -173,6 +179,7 @@ Navigate =
return if view is 'catalog' or 'f' in [boardID, g.BOARD.ID] return if view is 'catalog' or 'f' in [boardID, g.BOARD.ID]
e.preventDefault() if e e.preventDefault() if e
Navigate.title = -> return
delete Index.pageNum delete Index.pageNum
@ -196,18 +203,20 @@ Navigate =
if view is 'index' if view is 'index'
if boardID is g.BOARD.ID if boardID is g.BOARD.ID
d.title = $('.boardTitle').textContent Navigate.title = -> d.title = $('.boardTitle').textContent
else else
Navigate.updateBoard boardID g.BOARD = new Board boardID
Navigate.title = -> Navigate.updateBoard boardID
Index.update pageNum Index.update pageNum
# Moving from index to thread or thread to thread # Moving from index to thread or thread to thread
else else
onload = (e) -> Navigate.load e Navigate.updateFavicon Favicon.SFW
{load} = Navigate
Navigate.req = $.ajax "//a.4cdn.org/#{boardID}/res/#{threadID}.json", Navigate.req = $.ajax "//a.4cdn.org/#{boardID}/res/#{threadID}.json",
onabort: onload onabort: load
onloadend: onload onloadend: load
setTimeout (-> setTimeout (->
if Navigate.req and !Navigate.notice if Navigate.req and !Navigate.notice
@ -221,13 +230,13 @@ Navigate =
delete Navigate.req delete Navigate.req
delete Navigate.notice delete Navigate.notice
if e.type is 'abort' if e.type is 'abort' or req.status isnt 200
req.onloadend = null req.onloadend = null
new Notice 'warning', "Failed to load thread.#{if req.status then " #{req.status}" else ''}"
return return
try try
if req.status is 200 Navigate.parse JSON.parse(req.response).posts
Navigate.parse JSON.parse(req.response).posts
catch err catch err
console.error 'Navigate failure:' console.error 'Navigate failure:'
console.log err console.log err
@ -270,7 +279,8 @@ Navigate =
Main.callbackNodes Thread, [thread] Main.callbackNodes Thread, [thread]
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
Navigate.ready 'Quote Threading', QuoteThreading.force, Conf['Quote Threading'] Navigate.ready 'Quote Threading', QuoteThreading.force, Conf['Quote Threading'] and not Conf['Unread Count']
Navigate.ready 'Unread Count', Unread.ready, Conf['Unread Count']
Navigate.buildThread() Navigate.buildThread()
Header.hashScroll.call window Header.hashScroll.call window
@ -281,7 +291,6 @@ Navigate =
$.add board, [Navigate.threadRoot, $.el 'hr'] $.add board, [Navigate.threadRoot, $.el 'hr']
if Conf['Unread Count'] if Conf['Unread Count']
Navigate.ready 'Unread Count', Unread.ready, not Conf['Quote Threading']
Unread.read() Unread.read()
Unread.update() Unread.update()

View File

@ -2,7 +2,7 @@ class Board
toString: -> @ID toString: -> @ID
constructor: (@ID) -> constructor: (@ID) ->
@threads = {} @threads = new SimpleDict
@posts = {} @posts = new SimpleDict
g.boards[@] = @ g.boards[@] = @

View File

@ -5,4 +5,5 @@
<%= grunt.file.read('src/General/lib/clone.class') %> <%= grunt.file.read('src/General/lib/clone.class') %>
<%= grunt.file.read('src/General/lib/databoard.class') %> <%= grunt.file.read('src/General/lib/databoard.class') %>
<%= grunt.file.read('src/General/lib/notice.class') %> <%= grunt.file.read('src/General/lib/notice.class') %>
<%= grunt.file.read('src/General/lib/randomaccesslist.class') %> <%= grunt.file.read('src/General/lib/randomaccesslist.class') %>
<%= grunt.file.read('src/General/lib/simpledict.class') %>

View File

@ -54,7 +54,7 @@ class Post
@parseFile that @parseFile that
@clones = [] @clones = []
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @ g.posts.push @fullID, thread.posts.push @, board.posts.push @, @
@kill() if that.isArchived @kill() if that.isArchived
parseComment: -> parseComment: ->
@ -208,9 +208,9 @@ class Post
collect: -> collect: ->
@kill() @kill()
delete g.posts[@fullID] g.posts.rm @fullID
delete @thread.posts[@] @thread.posts.rm @
delete @board.posts[@] @board.posts.rm @
addClone: (context) -> addClone: (context) ->
new Clone @, context new Clone @, context

View File

@ -1,19 +1,36 @@
class RandomAccessList class RandomAccessList
constructor: -> constructor: (items) ->
@length = 0 @length = 0
@push item for item in items if items
push: (item) -> push: (data) ->
{ID} = item {ID} = data
ID or= data.id
return if @[ID] return if @[ID]
{last} = @ {last} = @
@[ID] = item =
prev: last
next: null
data: data
ID: ID
item.prev = last item.prev = last
@[ID] = item
@last = if last @last = if last
last.next = item last.next = item
else else
@first = item @first = item
@length++ @length++
before: (root, item) ->
return if item.next is root
@rmi item
{prev} = root
root.prev = item
item.next = root
item.prev = prev
prev.next = item if prev
after: (root, item) -> after: (root, item) ->
return if item.prev is root return if item.prev is root
@ -24,7 +41,7 @@ class RandomAccessList
item.prev = root item.prev = root
item.next = next item.next = next
next.prev = item if next next.prev = item if next
prepend: (item) -> prepend: (item) ->
{first} = @ {first} = @
return if item is first or not @[item.ID] return if item is first or not @[item.ID]
@ -36,6 +53,11 @@ class RandomAccessList
shift: -> shift: ->
@rm @first.ID @rm @first.ID
order: ->
order = [item = @first]
order.push item while item = item.next
order
rm: (ID) -> rm: (ID) ->
item = @[ID] item = @[ID]

View File

@ -0,0 +1,16 @@
class SimpleDict
constructor: ->
@keys = []
push: (key, data) ->
key = "#{key}"
@keys.push key unless @[key]
@[key] = data
rm: (key) ->
key = "#{key}"
if (i = @keys.indexOf key) isnt -1
@keys.splice i, 1
delete @[key]
forEach: (fn) -> fn @[key] for key in [@keys...]

View File

@ -4,13 +4,13 @@ class Thread
constructor: (@ID, @board) -> constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}" @fullID = "#{@board}.#{@ID}"
@posts = {} @posts = new SimpleDict
@isSticky = false @isSticky = false
@isClosed = false @isClosed = false
@postLimit = false @postLimit = false
@fileLimit = false @fileLimit = false
g.threads[@fullID] = board.threads[@] = @ g.threads.push @fullID, board.threads.push @, @
setPage: (pageNum) -> setPage: (pageNum) ->
icon = $ '.page-num', @OP.nodes.post icon = $ '.page-num', @OP.nodes.post
@ -44,7 +44,6 @@ class Thread
@timeOfDeath = Date.now() @timeOfDeath = Date.now()
collect: -> collect: ->
for postID, post in @posts @posts.forEach (post) -> post.collect()
post.collect() g.threads.rm @fullID
delete g.threads[@fullID] @board.threads.rm @
delete @board.threads[@]

View File

@ -35,6 +35,14 @@ ImageExpand =
ImageExpand.toggle Get.postFromNode @ ImageExpand.toggle Get.postFromNode @
toggleAll: -> toggleAll: ->
$.event 'CloseMenu' $.event 'CloseMenu'
func = (post) ->
{file} = post
return unless file and file.isImage and doc.contains post.nodes.root
if ImageExpand.on and
(!Conf['Expand spoilers'] and file.isSpoiler or
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
return
$.queueTask func, post
if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut' if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut'
ImageExpand.EAI.className = 'contract-all-shortcut a-icon' ImageExpand.EAI.className = 'contract-all-shortcut a-icon'
ImageExpand.EAI.title = 'Contract All Images' ImageExpand.EAI.title = 'Contract All Images'
@ -43,16 +51,10 @@ ImageExpand =
ImageExpand.EAI.className = 'expand-all-shortcut a-icon' ImageExpand.EAI.className = 'expand-all-shortcut a-icon'
ImageExpand.EAI.title = 'Expand All Images' ImageExpand.EAI.title = 'Expand All Images'
func = ImageExpand.contract func = ImageExpand.contract
for ID, post of g.posts g.posts.forEach (post) ->
for post in [post].concat post.clones func post
{file} = post func post for post in post.clones
continue unless file and file.isImage and doc.contains post.nodes.root return
if ImageExpand.on and
(!Conf['Expand spoilers'] and file.isSpoiler or
Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
continue
$.queueTask func, post
return
setFitness: -> setFitness: ->
(if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-' (if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-'

View File

@ -7,6 +7,10 @@ ImageLoader =
name: 'Image Replace' name: 'Image Replace'
cb: @node cb: @node
Thread.callbacks.push
name: 'Image Replace'
cb: @thread
return unless Conf['Image Prefetching'] and g.VIEW is 'thread' return unless Conf['Image Prefetching'] and g.VIEW is 'thread'
prefetch = $.el 'label', prefetch = $.el 'label',
@ -19,6 +23,9 @@ ImageLoader =
type: 'header' type: 'header'
el: prefetch el: prefetch
order: 104 order: 104
thread: ->
ImageLoader.thread = @
node: -> node: ->
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
@ -38,5 +45,5 @@ ImageLoader =
toggle: -> toggle: ->
enabled = Conf['prefetch'] = @checked enabled = Conf['prefetch'] = @checked
if enabled if enabled
ImageLoader.node.call post for id, post of g.threads["#{g.BOARD.ID}.#{g.THREADID}"].posts ImageLoader.thread.posts.forEach ImageLoader.node.call
return return

View File

@ -19,9 +19,8 @@ ExpandThread =
onIndexRefresh: -> onIndexRefresh: ->
ExpandThread.disconnect true ExpandThread.disconnect true
for threadID, thread of g.BOARD.threads g.BOARD.threads.forEach (thread) ->
ExpandThread.setButton thread ExpandThread.setButton thread
return
text: (status, posts, files) -> text: (status, posts, files) ->
"#{status} #{posts} post#{if posts > 1 then 's' else ''}" + "#{status} #{posts} post#{if posts > 1 then 's' else ''}" +

View File

@ -1,54 +1,63 @@
Favicon = Favicon =
init: -> init: ->
t = 'data:image/png;base64,' items = {
ferongr: [
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>'
]
'xat-': [
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
]
Mayhem: [
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
]
'4chanJS': [
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFWY.png", {encoding: "base64"}) %>'
]
Original: [
'<%= grunt.file.read("src/General/img/favicons/Original/unreadDead.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFW.png", {encoding: "base64"}) %>'
'<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>'
]
}[Conf['favicon']]
f = Favicon f = Favicon
[f.unreadDead, funreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = switch Conf['favicon'] t = 'data:image/png;base64,'
when 'ferongr' then [ i = 0
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>' while items[i]
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>' items[i] = t + items[i++]
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFW.png", {encoding: "base64"}) %>' [f.unreadDead, funreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = items
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>' f.update()
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>' update: ->
] if @SFW
when 'xat-' then [ @unread = @unreadSFW
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>' @unreadY = @unreadSFWY
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
]
when 'Mayhem' then [
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
]
when '4chanJS' then [
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDead.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadDeadY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadSFWY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/4chanJS/unreadNSFWY.png", {encoding: "base64"}) %>'
]
when 'Original' then [
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadDead.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFW.png", {encoding: "base64"}) %>'
t + '<%= grunt.file.read("src/General/img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>'
]
if Favicon.SFW
Favicon.unread = Favicon.unreadSFW
Favicon.unreadY = Favicon.unreadSFWY
else else
Favicon.unread = Favicon.unreadNSFW @unread = @unreadNSFW
Favicon.unreadY = Favicon.unreadNSFWY @unreadY = @unreadNSFWY
dead: 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/dead.gif", {encoding: "base64"}) %>' dead: 'data:image/gif;base64,<%= grunt.file.read("src/General/img/favicons/dead.gif", {encoding: "base64"}) %>'
logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>' logo: 'data:image/png;base64,<%= grunt.file.read("src/General/img/icon128.png", {encoding: "base64"}) %>'

View File

@ -26,7 +26,7 @@ ThreadStats =
node: -> node: ->
postCount = 0 postCount = 0
fileCount = 0 fileCount = 0
for ID, post of @posts @posts.forEach (post) ->
postCount++ postCount++
fileCount++ if post.file fileCount++ if post.file
ThreadStats.thread = @ ThreadStats.thread = @

View File

@ -291,11 +291,11 @@ ThreadUpdater =
deletedFiles = [] deletedFiles = []
# Check for deleted posts/files. # Check for deleted posts/files.
for ID, post of ThreadUpdater.thread.posts ThreadUpdater.thread.posts.forEach (post) ->
# XXX tmp fix for 4chan's racing condition # XXX tmp fix for 4chan's racing condition
# giving us false-positive dead posts. # giving us false-positive dead posts.
# continue if post.isDead # continue if post.isDead
ID = +ID ID = +post.ID
unless ID in index unless ID in index
post.kill() post.kill()
@ -306,6 +306,7 @@ ThreadUpdater =
post.kill true post.kill true
deletedFiles.push post deletedFiles.push post
# Fetching your own posts after posting
if ThreadUpdater.postID and ThreadUpdater.postID is ID if ThreadUpdater.postID and ThreadUpdater.postID is ID
ThreadUpdater.foundPost = true ThreadUpdater.foundPost = true
@ -326,8 +327,7 @@ ThreadUpdater =
scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and
ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25 ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
for key, post of posts for post in posts
continue unless posts.hasOwnProperty key
root = post.nodes.root root = post.nodes.root
if post.cb if post.cb
unless post.cb() unless post.cb()

View File

@ -174,7 +174,9 @@ ThreadWatcher =
$.rmAll list $.rmAll list
$.add list, nodes $.add list, nodes
for threadID, thread of g.BOARD.threads {threads} = g.BOARD
for threadID in threads.keys
thread = threads[threadID]
toggler = $ '.watch-thread-link', thread.OP.nodes.post toggler = $ '.watch-thread-link', thread.OP.nodes.post
watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID} watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch'] helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch']

View File

@ -42,7 +42,7 @@ Unread =
ready: -> ready: ->
$.off d, '4chanXInitFinished', Unread.ready $.off d, '4chanXInitFinished', Unread.ready
posts = [] posts = []
posts.push post for ID, post of Unread.thread.posts when post.isReply Unread.thread.posts.forEach (post) -> posts.push post if post.isReply
Unread.addPosts posts unless Conf['Quote Threading'] Unread.addPosts posts unless Conf['Quote Threading']
QuoteThreading.force() if Conf['Quote Threading'] QuoteThreading.force() if Conf['Quote Threading']
Unread.scroll() if Conf['Scroll to Last Read Post'] Unread.scroll() if Conf['Scroll to Last Read Post']
@ -52,14 +52,15 @@ Unread =
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
if post = Unread.posts.first if post = Unread.posts.first
# Scroll to a non-hidden, non-OP post that's before the first unread post. # 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 while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.data.nodes.root
break unless (post = Get.postFromRoot root).isHidden break unless (post = Get.postFromRoot root).isHidden
return unless root return unless root
down = true down = true
else else
# Scroll to the last read post. # Scroll to the last read post.
posts = Object.keys Unread.thread.posts {posts} = Unread.thread
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes {keys} = posts
{root} = posts[keys[keys.length - 1]].nodes
# Scroll to the target unless we scrolled past it. # Scroll to the target unless we scrolled past it.
Header.scrollTo root, down if Header.getBottomOf(root) < 0 Header.scrollTo root, down if Header.getBottomOf(root) < 0
@ -94,12 +95,11 @@ Unread =
Unread.addPostQuotingYou post Unread.addPostQuotingYou post
if Conf['Unread Line'] if Conf['Unread Line']
# Force line on visible threads if there were no unread posts previously. # Force line on visible threads if there were no unread posts previously.
Unread.setLine Unread.posts.first in posts Unread.setLine Unread.posts.first?.data in posts
Unread.read() Unread.read()
Unread.update() Unread.update()
addPostQuotingYou: (post) -> addPostQuotingYou: (post) ->
return unless QR.db
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
Unread.postsQuotingYou.push post Unread.postsQuotingYou.push post
Unread.openNotification post Unread.openNotification post
@ -130,11 +130,12 @@ Unread =
readSinglePost: (post) -> readSinglePost: (post) ->
{ID} = post {ID} = post
return unless Unread.posts[ID] {posts} = Unread
if post is Unread.posts.first return unless posts[ID]
if post is posts.first
Unread.lastReadPost = ID Unread.lastReadPost = ID
Unread.saveLastReadPost() Unread.saveLastReadPost()
Unread.posts.rm ID posts.rm ID
if (i = Unread.postsQuotingYou.indexOf post) isnt -1 if (i = Unread.postsQuotingYou.indexOf post) isnt -1
Unread.postsQuotingYou.splice i, 1 Unread.postsQuotingYou.splice i, 1
Unread.update() Unread.update()
@ -150,12 +151,16 @@ Unread =
{posts} = Unread {posts} = Unread
while post = posts.first while post = posts.first
break unless Header.getBottomOf(post.nodes.root) > -1 # post is not completely read break unless Header.getBottomOf(post.data.nodes.root) > -1 # post is not completely read
{ID} = post {ID, data} = post
posts.rm ID posts.rm ID
if Conf['Mark Quotes of You'] and post.info.yours if Conf['Mark Quotes of You'] and QR.db.get {
QuoteYou.lastRead = post.nodes.root boardID: data.board.ID
threadID: data.thread.ID
postID: ID
}
QuoteYou.lastRead = data.nodes.root
return unless ID return unless ID
@ -174,8 +179,8 @@ Unread =
setLine: (force) -> setLine: (force) ->
return unless d.hidden or force is true return unless d.hidden or force is true
return $.rm Unread.hr unless post = Unread.posts.first return $.rm Unread.hr unless post = Unread.posts.first
if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.nodes.root # not the first reply if $.x 'preceding-sibling::div[contains(@class,"replyContainer")]', post.data.nodes.root # not the first reply
$.before post.nodes.root, Unread.hr $.before post.data.nodes.root, Unread.hr
update: <% if (type === 'crx') { %>(dontrepeat) <% } %>-> update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
count = Unread.posts.length count = Unread.posts.length

View File

@ -324,7 +324,7 @@ QR =
return unless QR.nodes return unless QR.nodes
list = QR.nodes.thread list = QR.nodes.thread
options = [list.firstChild] options = [list.firstChild]
for thread of g.BOARD.threads for thread in g.BOARD.threads.keys
options.push $.el 'option', options.push $.el 'option',
value: thread value: thread
textContent: "Thread No.#{thread}" textContent: "Thread No.#{thread}"

View File

@ -42,15 +42,19 @@ QuoteThreading =
QuoteThreading.force() QuoteThreading.force()
force: -> force: ->
post.cb true for ID, post of g.posts when post.cb g.posts.forEach (post) ->
return post.cb true if post.cb
if Conf['Unread Count'] and Unread.thread.OP.nodes.root.parentElement.parentElement
Unread.read()
Unread.update()
node: -> node: ->
{posts} = g {posts} = g
return if @isClone or not QuoteThreading.enabled return if @isClone or not QuoteThreading.enabled
Unread.posts.push @ if Conf['Unread Count']
return if @thread.OP is @ or !(post = posts[@fullID]) or post.isHidden # Filtered Unread.posts.push @ if Conf['Unread Count']
return if @thread.OP is @ or @isHidden # Filtered
keys = [] keys = []
len = g.BOARD.ID.length + 1 len = g.BOARD.ID.length + 1
@ -90,11 +94,11 @@ QuoteThreading =
return true unless Conf['Unread Count'] return true unless Conf['Unread Count']
if posts[post.ID] if post = posts[post.ID]
posts.after post, @ posts.after post, posts[@ID]
else else
posts.prepend @ posts.prepend posts[@ID]
return true return true
@ -106,8 +110,10 @@ QuoteThreading =
thread = $('.thread') thread = $('.thread')
posts = [] posts = []
nodes = [] nodes = []
g.posts.forEach (post) ->
posts.push post unless post is post.thread.OP or post.isClone
posts.push post for ID, post of g.posts when not (post is post.thread.OP or post.isClone)
posts.sort (a, b) -> a.ID - b.ID posts.sort (a, b) -> a.ID - b.ID
nodes.push post.nodes.root for post in posts nodes.push post.nodes.root for post in posts