4chan-x/src/General/Index.coffee

966 lines
33 KiB
CoffeeScript

Index =
showHiddenThreads: false
changed: {}
enabledOn: ({siteID, boardID}) ->
Conf['JSON Index'] and g.sites[siteID].software is 'yotsuba' and boardID isnt 'f'
init: ->
return unless g.VIEW is 'index'
# For IndexRefresh events
$.one d, '4chanXInitFinished', @cb.initFinished
$.on d, 'PostsInserted', @cb.postsInserted
return unless @enabledOn g.BOARD
@enabled = true
Callbacks.Post.push
name: 'Index Page Numbers'
cb: @node
Callbacks.CatalogThread.push
name: 'Catalog Features'
cb: @catalogNode
@search = history.state?.searched or ''
if history.state?.mode
Conf['Index Mode'] = history.state?.mode
@currentSort = history.state?.sort
@currentSort or=
if typeof Conf['Index Sort'] is 'object' then (
Conf['Index Sort'][g.BOARD.ID] or 'bump'
) else (
Conf['Index Sort']
)
@currentPage = @getCurrentPage()
@processHash()
$.addClass doc, 'index-loading', "#{Conf['Index Mode'].replace /\ /g, '-'}-mode"
$.on window, 'popstate', @cb.popstate
$.on d, 'scroll', @scroll
$.on d, 'SortIndex', @cb.resort
# Header refresh button
@button = $.el 'a',
className: 'fa fa-refresh'
title: 'Refresh'
href: 'javascript:;'
textContent: 'Refresh Index'
$.on @button, 'click', -> Index.update()
Header.addShortcut 'index-refresh', @button, 590
# Header "Index Navigation" submenu
entries = []
@inputs = inputs = {}
for name, arr of Config.Index when arr instanceof Array
label = UI.checkbox name, "#{name[0]}#{name[1..].toLowerCase()}"
label.title = arr[1]
entries.push {el: label}
input = label.firstChild
$.on input, 'change', $.cb.checked
inputs[name] = input
$.on inputs['Show Replies'], 'change', @cb.replies
$.on inputs['Catalog Hover Expand'], 'change', @cb.hover
$.on inputs['Pin Watched Threads'], 'change', @cb.resort
$.on inputs['Anchor Hidden Threads'], 'change', @cb.resort
watchSettings = (e) ->
if (input = inputs[e.target.name])
input.checked = e.target.checked
$.event 'change', null, input
$.on d, 'OpenSettings', ->
$.on $.id('fourchanx-settings'), 'change', watchSettings
sortEntry = UI.checkbox 'Per-Board Sort Type', 'Per-board sort type', (typeof Conf['Index Sort'] is 'object')
sortEntry.title = 'Set the sorting order of each board independently.'
$.on sortEntry.firstChild, 'change', @cb.perBoardSort
entries.splice 3, 0, {el: sortEntry}
Header.menu.addEntry
el: $.el 'span',
textContent: 'Index Navigation'
order: 100
subEntries: entries
# Navigation links at top of index
@navLinks = $.el 'div', className: 'navLinks json-index'
$.extend @navLinks, `<%= readHTML('NavLinks.html') %>`
$('.cataloglink a', @navLinks).href = CatalogLinks.catalog()
$('.archlistlink', @navLinks).hidden = true unless BoardConfig.isArchived(g.BOARD.ID)
$.on $('#index-last-refresh a', @navLinks), 'click', @cb.refreshFront
# Search field
@searchInput = $ '#index-search', @navLinks
@setupSearch()
$.on @searchInput, 'input', @onSearchInput
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
# Hidden threads toggle
@hideLabel = $ '#hidden-label', @navLinks
$.on $('#hidden-toggle a', @navLinks), 'click', @cb.toggleHiddenThreads
# Drop-down menus and reverse sort toggle
@selectRev = $ '#index-rev', @navLinks
@selectMode = $ '#index-mode', @navLinks
@selectSort = $ '#index-sort', @navLinks
@selectSize = $ '#index-size', @navLinks
$.on @selectRev, 'change', @cb.sort
$.on @selectMode, 'change', @cb.mode
$.on @selectSort, 'change', @cb.sort
$.on @selectSize, 'change', $.cb.value
$.on @selectSize, 'change', @cb.size
for select in [@selectMode, @selectSize]
select.value = Conf[select.name]
@selectRev.checked = /-rev$/.test Index.currentSort
@selectSort.value = Index.currentSort.replace /-rev$/, ''
# Last Long Reply options
@lastLongOptions = $ '#lastlong-options', @navLinks
@lastLongInputs = $$ 'input', @lastLongOptions
@lastLongThresholds = [0, 0]
@lastLongOptions.hidden = (@selectSort.value isnt 'lastlong')
for input, i in @lastLongInputs
$.on input, 'change', @cb.lastLongThresholds
tRaw = Conf["Last Long Reply Thresholds #{i}"]
input.value = @lastLongThresholds[i] =
if typeof tRaw is 'object' then (tRaw[g.BOARD.ID] ? 100) else tRaw
# Thread container
@root = $.el 'div', className: 'board json-index'
$.on @root, 'click', @cb.hoverToggle
@cb.size()
@cb.hover()
# Page list
@pagelist = $.el 'div', className: 'pagelist json-index'
$.extend @pagelist, `<%= readHTML('PageList.html') %>`
$('.cataloglink a', @pagelist).href = CatalogLinks.catalog()
$.on @pagelist, 'click', @cb.pageNav
@update true
$.onExists doc, 'title + *', ->
d.title = d.title.replace /\ -\ Page\ \d+/, ''
$.onExists doc, '.board > .thread > .postContainer, .board + *', ->
g.SITE.Build.hat = $ '.board > .thread > img:first-child'
if g.SITE.Build.hat
g.BOARD.threads.forEach (thread) ->
if thread.nodes.root
$.prepend thread.nodes.root, g.SITE.Build.hat.cloneNode false
$.addClass doc, 'hats-enabled'
$.addStyle ".catalog-thread::after {background-image: url(#{g.SITE.Build.hat.src});}"
board = $ '.board'
$.replace board, Index.root
if Index.loaded
$.event 'PostsInserted', null, Index.root
# Hacks:
# - When removing an element from the document during page load,
# its ancestors will still be correctly created inside of it.
# - Creating loadable elements inside of an origin-less document
# will not download them.
# - Combine the two and you get a download canceller!
# Does not work on Firefox unfortunately. bugzil.la/939713
try
d.implementation.createDocument(null, null, null).appendChild board
$.rm el for el in $$ '.navLinks'
$.rm $.id('ctrl-top')
topNavPos = $.id('delform').previousElementSibling
$.before topNavPos, $.el 'hr'
$.before topNavPos, Index.navLinks
timeEl = $ '#index-last-refresh time', Index.navLinks
RelativeDates.update timeEl if timeEl.dataset.utc
Main.ready ->
if (pagelist = $ '.pagelist')
$.replace pagelist, Index.pagelist
$.rmClass doc, 'index-loading'
scroll: ->
return if Index.req or !Index.liveThreadData or Conf['Index Mode'] isnt 'infinite' or (window.scrollY <= doc.scrollHeight - (300 + window.innerHeight))
Index.pageNum ?= Index.currentPage # Avoid having to pushState to keep track of the current page
pageNum = ++Index.pageNum
return Index.endNotice() if pageNum > Index.pagesNum
threadIDs = Index.threadsOnPage pageNum
Index.buildStructure threadIDs
endNotice: do ->
notify = false
reset = -> notify = false
return ->
return if notify
notify = true
new Notice 'info', "Last page reached.", 2
setTimeout reset, 3 * $.SECOND
menu:
init: ->
return unless g.VIEW is 'index' and Conf['Menu'] and Conf['Thread Hiding Link'] and Index.enabledOn(g.BOARD)
Menu.menu.addEntry
el: $.el 'a',
href: 'javascript:;'
className: 'has-shortcut-text'
, `<%= html('<span></span><span class="shortcut-text">Shift+click</span>') %>`
order: 20
open: ({thread}) ->
return false if Conf['Index Mode'] isnt 'catalog'
@el.firstElementChild.textContent = if thread.isHidden
'Unhide'
else
'Hide'
$.off @el, 'click', @cb if @cb
@cb = ->
$.event 'CloseMenu'
Index.toggleHide thread
$.on @el, 'click', @cb
true
node: ->
return if @isReply or @isClone or not (Index.threadPosition[@ID]?)
@thread.setPage(Index.threadPosition[@ID] // Index.threadsNumPerPage + 1)
catalogNode: ->
$.on @nodes.root, 'mousedown click', (e) =>
return unless e.button is 0 and e.shiftKey
Index.toggleHide @thread if e.type is 'click'
e.preventDefault() # Also on mousedown to prevent highlighting text.
toggleHide: (thread) ->
if Index.showHiddenThreads
ThreadHiding.show thread
return unless ThreadHiding.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Don't save when un-hiding filtered threads.
else
ThreadHiding.hide thread
ThreadHiding.saveHiddenState thread
cycleSortType: ->
types = [Index.selectSort.options...].filter (option) -> !option.disabled
for type, i in types
break if type.selected
types[(i + 1) % types.length].selected = true
$.event 'change', null, Index.selectSort
cb:
initFinished: ->
Index.initFinishedFired = true
$.queueTask -> Index.cb.postsInserted()
postsInserted: ->
return unless Index.initFinishedFired
n = 0
g.posts.forEach (post) ->
if !post.isFetchedQuote and !post.indexRefreshSeen and doc.contains(post.nodes.root)
post.indexRefreshSeen = true
n++
$.event 'IndexRefresh' if n
toggleHiddenThreads: ->
$('#hidden-toggle a', Index.navLinks).textContent = if Index.showHiddenThreads = !Index.showHiddenThreads
'Hide'
else
'Show'
Index.sort()
Index.buildIndex()
mode: ->
Index.pushState {mode: @value}
Index.pageLoad false
sort: ->
value = if Index.selectRev.checked then Index.selectSort.value + "-rev" else Index.selectSort.value
Index.pushState {sort: value}
Index.pageLoad false
resort: (e) ->
Index.changed.order = true
Index.pageLoad false unless e?.detail?.deferred
perBoardSort: ->
Conf['Index Sort'] = if @checked then {} else ''
Index.saveSort()
for i in [0...2]
Conf["Last Long Reply Thresholds #{i}"] = if @checked then {} else ''
Index.saveLastLongThresholds i
return
lastLongThresholds: ->
i = [@parentNode.children...].indexOf @
value = +@value
unless Number.isFinite(value)
@value = Index.lastLongThresholds[i]
return
Index.lastLongThresholds[i] = value
Index.saveLastLongThresholds i
Index.changed.order = true
Index.pageLoad false
size: (e) ->
if Conf['Index Mode'] isnt 'catalog'
$.rmClass Index.root, 'catalog-small'
$.rmClass Index.root, 'catalog-large'
else if Conf['Index Size'] is 'small'
$.addClass Index.root, 'catalog-small'
$.rmClass Index.root, 'catalog-large'
else
$.addClass Index.root, 'catalog-large'
$.rmClass Index.root, 'catalog-small'
Index.buildIndex() if e
replies: ->
Index.buildIndex()
hover: ->
doc.classList.toggle 'catalog-hover-expand', Conf['Catalog Hover Expand']
hoverToggle: (e) ->
if Conf['Catalog Hover Toggle'] and $.hasClass(doc, 'catalog-mode') and !$.modifiedClick(e) and !$.x('ancestor-or-self::a', e.target)
input = Index.inputs['Catalog Hover Expand']
input.checked = !input.checked
$.event 'change', null, input
if (thread = Get.threadFromNode e.target)
Index.cb.catalogReplies.call thread
Index.cb.hoverAdjust.call thread.OP.nodes
popstate: (e) ->
if e?.state
{searched, mode, sort} = e.state
page = Index.getCurrentPage()
Index.setState {search: searched, mode, sort, page}
Index.pageLoad false
else
# page load or hash change
nCommands = Index.processHash()
if Conf['Refreshed Navigation'] and nCommands
Index.update()
else
Index.pageLoad()
pageNav: (e) ->
return if $.modifiedClick e
switch e.target.nodeName
when 'BUTTON'
e.target.blur()
a = e.target.parentNode
when 'A'
a = e.target
else
return
return if a.textContent is 'Catalog'
e.preventDefault()
Index.userPageNav +a.pathname.split(/\/+/)[2] or 1
refreshFront: ->
Index.pushState {page: 1}
Index.update()
catalogReplies: ->
if Conf['Show Replies'] and $.hasClass(doc, 'catalog-hover-expand') and !@catalogView.nodes.replies
Index.buildCatalogReplies @
hoverAdjust: ->
# Prevent hovered catalog threads from going offscreen.
return unless $.hasClass(doc, 'catalog-hover-expand')
rect = @post.getBoundingClientRect()
if (x = $.minmax 0, -rect.left, doc.clientWidth - rect.right)
{style} = @post
style.left = "#{x}px"
style.right = "#{-x}px"
$.one @root, 'mouseleave', -> style.left = style.right = null
scrollToIndex: ->
# Scroll to navlinks, or top of board if navlinks are hidden.
Header.scrollToIfNeeded (if Index.navLinks.getBoundingClientRect().height then Index.navLinks else Index.root)
getCurrentPage: ->
+window.location.pathname.split(/\/+/)[2] or 1
userPageNav: (page) ->
Index.pushState {page}
if Conf['Refreshed Navigation']
Index.update()
else
Index.pageLoad()
hashCommands:
mode:
'paged': 'paged'
'infinite-scrolling': 'infinite'
'infinite': 'infinite'
'all-threads': 'all pages'
'all-pages': 'all pages'
'catalog': 'catalog'
sort:
'bump-order': 'bump'
'last-reply': 'lastreply'
'last-long-reply': 'lastlong'
'creation-date': 'birth'
'reply-count': 'replycount'
'file-count': 'filecount'
processHash: ->
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=483304
hash = location.href.match(/#.*/)?[0] or ''
state =
replace: true
commands = hash[1..].split '/'
leftover = []
for command in commands
if (mode = Index.hashCommands.mode[command])
state.mode = mode
else if command is 'index'
state.mode = Conf['Previous Index Mode']
state.page = 1
else if (sort = Index.hashCommands.sort[command.replace(/-rev$/, '')])
state.sort = sort
state.sort += '-rev' if /-rev$/.test(command)
else if /^s=/.test command
state.search = decodeURIComponent(command[2..]).replace(/\+/g, ' ').trim()
else
leftover.push command
hash = leftover.join '/'
state.hash = "##{hash}" if hash
Index.pushState state
commands.length - leftover.length
pushState: (state) ->
{search, hash, replace} = state
pageBeforeSearch = history.state?.oldpage
if search? and search isnt Index.search
state.page = if search then 1 else (pageBeforeSearch or 1)
if !search
pageBeforeSearch = undefined
else if !Index.search
pageBeforeSearch = Index.currentPage
Index.setState state
pathname = if Index.currentPage is 1 then "/#{g.BOARD}/" else "/#{g.BOARD}/#{Index.currentPage}"
hash or= ''
history[if replace then 'replaceState' else 'pushState']
mode: Conf['Index Mode']
sort: Index.currentSort
searched: Index.search
oldpage: pageBeforeSearch
, '', "#{location.protocol}//#{location.host}#{pathname}#{hash}"
setState: ({search, mode, sort, page, hash}) ->
if search? and search isnt Index.search
Index.changed.search = true
Index.search = search
if mode? and mode isnt Conf['Index Mode']
Index.changed.mode = true
Conf['Index Mode'] = mode
$.set 'Index Mode', mode
unless mode is 'catalog' or Conf['Previous Index Mode'] is mode
Conf['Previous Index Mode'] = mode
$.set 'Previous Index Mode', mode
if sort? and sort isnt Index.currentSort
Index.changed.sort = true
Index.currentSort = sort
Index.saveSort()
page = 1 if Conf['Index Mode'] in ['all pages', 'catalog']
if page? and page isnt Index.currentPage
Index.changed.page = true
Index.currentPage = page
if hash?
Index.changed.hash = true
savePerBoard: (key, value) ->
if typeof Conf[key] is 'object'
Conf[key][g.BOARD.ID] = value
else
Conf[key] = value
$.set key, Conf[key]
saveSort: ->
Index.savePerBoard 'Index Sort', Index.currentSort
saveLastLongThresholds: (i) ->
Index.savePerBoard "Last Long Reply Thresholds #{i}", Index.lastLongThresholds[i]
pageLoad: (scroll=true) ->
return unless Index.liveThreadData
{threads, order, search, mode, sort, page, hash} = Index.changed
threads or= search
order or= sort
Index.sort() if threads or order
Index.buildPagelist() if threads
Index.setupSearch() if search
Index.setupMode() if mode
Index.setupSort() if sort
Index.buildIndex() if threads or mode or page or order
Index.setPage() if threads or page
Index.scrollToIndex() if scroll and not hash
Header.hashScroll() if hash
Index.changed = {}
setupMode: ->
for mode in ['paged', 'infinite', 'all pages', 'catalog']
$[if mode is Conf['Index Mode'] then 'addClass' else 'rmClass'] doc, "#{mode.replace /\ /g, '-'}-mode"
Index.selectMode.value = Conf['Index Mode']
Index.cb.size()
Index.showHiddenThreads = false
$('#hidden-toggle a', Index.navLinks).textContent = 'Show'
setupSort: ->
Index.selectRev.checked = /-rev$/.test Index.currentSort
Index.selectSort.value = Index.currentSort.replace /-rev$/, ''
Index.lastLongOptions.hidden = (Index.selectSort.value isnt 'lastlong')
getPagesNum: ->
if Index.search
Math.ceil Index.sortedThreadIDs.length / Index.threadsNumPerPage
else
Index.pagesNum
getMaxPageNum: ->
Math.max 1, Index.getPagesNum()
buildPagelist: ->
pagesRoot = $ '.pages', Index.pagelist
maxPageNum = Index.getMaxPageNum()
if pagesRoot.childElementCount isnt maxPageNum
nodes = []
for i in [1..maxPageNum] by 1
a = $.el 'a',
textContent: i
href: if i is 1 then './' else i
nodes.push $.tn('['), a, $.tn '] '
$.rmAll pagesRoot
$.add pagesRoot, nodes
setPage: ->
pageNum = Index.currentPage
maxPageNum = Index.getMaxPageNum()
pagesRoot = $ '.pages', Index.pagelist
# Previous/Next buttons
prev = pagesRoot.previousSibling.firstChild
next = pagesRoot.nextSibling.firstChild
href = Math.max pageNum - 1, 1
prev.href = if href is 1 then './' else href
prev.firstChild.disabled = href is pageNum
href = Math.min pageNum + 1, maxPageNum
next.href = if href is 1 then './' else href
next.firstChild.disabled = href is pageNum
# <strong> current page
if strong = $ 'strong', pagesRoot
return if +strong.textContent is pageNum
$.replace strong, strong.firstChild
else
strong = $.el 'strong'
if (a = pagesRoot.children[pageNum - 1])
$.before a, strong
$.add strong, a
updateHideLabel: ->
return unless Index.hideLabel
hiddenCount = 0
for threadID in Index.liveThreadIDs when Index.isHidden(threadID)
hiddenCount++
unless hiddenCount
Index.hideLabel.hidden = true
Index.cb.toggleHiddenThreads() if Index.showHiddenThreads
return
Index.hideLabel.hidden = false
$('#hidden-count', Index.navLinks).textContent = if hiddenCount is 1
'1 hidden thread'
else
"#{hiddenCount} hidden threads"
update: (firstTime) ->
if (oldReq = Index.req)
delete Index.req
oldReq.abort()
if Conf['Index Refresh Notifications']
# Optional notification for manual refreshes
Index.notice or= new Notice 'info', 'Refreshing index...'
else
# Also display notice if Index Refresh is taking too long
Index.nTimeout or= setTimeout ->
Index.notice or= new Notice 'info', 'Refreshing index...'
, 3 * $.SECOND
# Hard refresh in case of incomplete page load.
if not firstTime and d.readyState isnt 'loading' and not $('.board + *')
location.reload()
return
Index.req = $.whenModified(
g.SITE.urls.catalogJSON({boardID: g.BOARD.ID}),
'Index',
Index.load
)
$.addClass Index.button, 'fa-spin'
load: ->
return if @ isnt Index.req # aborted
$.rmClass Index.button, 'fa-spin'
{notice, nTimeout} = Index
clearTimeout nTimeout if nTimeout
delete Index.nTimeout
delete Index.req
delete Index.notice
if @status not in [200, 304]
err = "Index refresh failed. #{if @status then "Error #{@statusText} (#{@status})" else 'Connection Error'}"
if notice
notice.setType 'warning'
notice.el.lastElementChild.textContent = err
setTimeout notice.close, $.SECOND
else
new Notice 'warning', err, 1
return
try
if @status is 200
Index.parse @response
else if @status is 304
Index.pageLoad()
catch err
c.error "Index failure: #{err.message}", err.stack
if notice
notice.setType 'error'
notice.el.lastElementChild.textContent = 'Index refresh failed.'
setTimeout notice.close, $.SECOND
else
new Notice 'error', 'Index refresh failed.', 1
return
if notice
if Conf['Index Refresh Notifications']
notice.setType 'success'
notice.el.lastElementChild.textContent = 'Index refreshed!'
setTimeout notice.close, $.SECOND
else
notice.close()
timeEl = $ '#index-last-refresh time', Index.navLinks
timeEl.dataset.utc = Date.parse @getResponseHeader 'Last-Modified'
RelativeDates.update timeEl
parse: (pages) ->
$.cleanCache (url) -> /^https?:\/\/a\.4cdn\.org\//.test url
Index.parseThreadList pages
Index.changed.threads = true
Index.pageLoad()
parseThreadList: (pages) ->
Index.pagesNum = pages.length
Index.threadsNumPerPage = pages[0]?.threads.length or 1
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
Index.liveThreadDict = {}
Index.threadPosition = {}
Index.parsedThreads = {}
Index.replyData = {}
for data, i in Index.liveThreadData
Index.liveThreadDict[data.no] = data
Index.threadPosition[data.no] = i
Index.parsedThreads[data.no] = obj = g.SITE.Build.parseJSON data, g.BOARD
obj.filterResults = results = Filter.test obj
obj.isOnTop = results.top
obj.isHidden = results.hide or ThreadHiding.isHidden(obj.boardID, obj.threadID)
if data.last_replies
for reply in data.last_replies
Index.replyData["#{g.BOARD}.#{reply.no}"] = reply
if Index.liveThreadData[0]
g.SITE.Build.spoilerRange[g.BOARD.ID] = Index.liveThreadData[0].custom_spoiler
g.BOARD.threads.forEach (thread) ->
(thread.collect() unless thread.ID in Index.liveThreadIDs)
$.event 'IndexUpdate',
threads: ("#{g.BOARD}.#{ID}" for ID in Index.liveThreadIDs)
return
isHidden: (threadID) ->
if (thread = g.BOARD.threads[threadID]) and thread.OP and not thread.OP.isFetchedQuote
thread.isHidden
else
Index.parsedThreads[threadID].isHidden
isHiddenReply: (threadID, replyData) ->
PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) or Filter.isHidden(g.SITE.Build.parseJSON replyData, g.BOARD)
buildThreads: (threadIDs, isCatalog, withReplies) ->
threads = []
newThreads = []
newPosts = []
for ID in threadIDs
try
threadData = Index.liveThreadDict[ID]
if (thread = g.BOARD.threads[ID])
isStale = (thread.json isnt threadData) and (JSON.stringify(thread.json) isnt JSON.stringify(threadData))
if isStale
thread.setCount 'post', threadData.replies + 1, threadData.bumplimit
thread.setCount 'file', threadData.images + !!threadData.ext, threadData.imagelimit
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
if thread.catalogView
$.rm thread.catalogView.nodes.replies
thread.catalogView.nodes.replies = null
else
thread = new Thread ID, g.BOARD
newThreads.push thread
lastPost = if threadData.last_replies then threadData.last_replies[threadData.last_replies.length - 1].no else ID
thread.lastPost = lastPost if lastPost > thread.lastPost
thread.json = threadData
threads.push thread
if ((OP = thread.OP) and not OP.isFetchedQuote)
OP.setCatalogOP isCatalog
thread.setPage(Index.threadPosition[ID] // Index.threadsNumPerPage + 1)
else
obj = Index.parsedThreads[ID]
OP = new Post g.SITE.Build.post(obj), thread, g.BOARD
OP.filterResults = obj.filterResults
newPosts.push OP
unless isCatalog and thread.nodes.root
g.SITE.Build.thread thread, threadData, withReplies
catch err
# Skip posts that we failed to parse.
errors = [] unless errors
errors.push
message: "Parsing of Thread No.#{thread} failed. Thread will be skipped."
error: err
Main.handleErrors errors if errors
if withReplies
newPosts = newPosts.concat(Index.buildReplies threads)
Main.callbackNodes 'Thread', newThreads
Main.callbackNodes 'Post', newPosts
Index.updateHideLabel()
$.event 'IndexRefreshInternal', {threadIDs: (t.fullID for t in threads), isCatalog}
threads
buildReplies: (threads) ->
posts = []
for thread in threads
continue if not (lastReplies = Index.liveThreadDict[thread.ID].last_replies)
nodes = []
for data in lastReplies
if (post = thread.posts[data.no]) and not post.isFetchedQuote
nodes.push post.nodes.root
continue
nodes.push node = g.SITE.Build.postFromObject data, thread.board.ID
try
posts.push new Post node, thread, thread.board
catch err
# Skip posts that we failed to parse.
errors = [] unless errors
errors.push
message: "Parsing of Post No.#{data.no} failed. Post will be skipped."
error: err
$.add thread.nodes.root, nodes
Main.handleErrors errors if errors
posts
buildCatalogViews: (threads) ->
catalogThreads = []
for thread in threads when !thread.catalogView
{ID} = thread
page = Index.threadPosition[ID] // Index.threadsNumPerPage + 1
root = g.SITE.Build.catalogThread thread, Index.liveThreadDict[ID], page
catalogThreads.push new CatalogThread root, thread
Main.callbackNodes 'CatalogThread', catalogThreads
return
sizeCatalogViews: (threads) ->
# XXX When browsers support CSS3 attr(), use it instead.
size = if Conf['Index Size'] is 'small' then 150 else 250
for thread in threads
{thumb} = thread.catalogView.nodes
{width, height} = thumb.dataset
continue unless width
ratio = size / Math.max width, height
thumb.style.width = width * ratio + 'px'
thumb.style.height = height * ratio + 'px'
return
buildCatalogReplies: (thread) ->
{nodes} = thread.catalogView
return if not (lastReplies = Index.liveThreadDict[thread.ID].last_replies)
replies = []
for data in lastReplies
continue if Index.isHiddenReply thread.ID, data
reply = g.SITE.Build.catalogReply thread, data
RelativeDates.update $('time', reply)
$.on $('.catalog-reply-preview', reply), 'mouseover', QuotePreview.mouseover
replies.push reply
nodes.replies = $.el 'div', className: 'catalog-replies'
$.add nodes.replies, replies
$.add thread.OP.nodes.post, nodes.replies
return
sort: ->
{liveThreadIDs, liveThreadData} = Index
return unless liveThreadData
sortType = Index.currentSort.replace(/-rev$/, '')
Index.sortedThreadIDs = switch sortType
when 'lastreply', 'lastlong'
lastlong = (thread) ->
for r, i in (thread.last_replies or []) by -1
continue if Index.isHiddenReply thread.no, r
if sortType is 'lastreply'
return r
len = if r.com then g.SITE.Build.parseComment(r.com).replace(/[^a-z]/ig, '').length else 0
if len >= Index.lastLongThresholds[+!!r.ext]
return r
if thread.omitted_posts then thread.last_replies[0] else thread
lastlongD = {}
for thread in liveThreadData
lastlongD[thread.no] = lastlong(thread).no
[liveThreadData...].sort((a, b) ->
lastlongD[b.no] - lastlongD[a.no]
).map (post) -> post.no
when 'bump' then liveThreadIDs
when 'birth' then [liveThreadIDs... ].sort (a, b) -> b - a
when 'replycount' then [liveThreadData...].sort((a, b) -> b.replies - a.replies).map (post) -> post.no
when 'filecount' then [liveThreadData...].sort((a, b) -> b.images - a.images ).map (post) -> post.no
else liveThreadIDs
if /-rev$/.test(Index.currentSort)
Index.sortedThreadIDs = [Index.sortedThreadIDs...].reverse()
if Index.search and (threadIDs = Index.querySearch Index.search)
Index.sortedThreadIDs = threadIDs
# Sticky threads
Index.sortOnTop (obj) -> obj.isSticky
# Highlighted threads
Index.sortOnTop (obj) -> obj.isOnTop or Conf['Pin Watched Threads'] and ThreadWatcher.isWatchedRaw(obj.boardID, obj.threadID)
# Non-hidden threads
Index.sortOnTop((obj) -> !Index.isHidden(obj.threadID)) if Conf['Anchor Hidden Threads']
sortOnTop: (match) ->
topThreads = []
bottomThreads = []
for ID in Index.sortedThreadIDs
(if match Index.parsedThreads[ID] then topThreads else bottomThreads).push ID
Index.sortedThreadIDs = topThreads.concat bottomThreads
buildIndex: ->
return unless Index.liveThreadData
switch Conf['Index Mode']
when 'all pages'
threadIDs = Index.sortedThreadIDs
when 'catalog'
threadIDs = Index.sortedThreadIDs.filter (ID) -> !Index.isHidden(ID) isnt Index.showHiddenThreads
else
threadIDs = Index.threadsOnPage Index.currentPage
delete Index.pageNum
$.rmAll Index.root
$.rmAll Header.hover
if Index.loaded and Index.root.parentNode
$.event 'PostsRemoved', null, Index.root
if Conf['Index Mode'] is 'catalog'
Index.buildCatalog threadIDs
else
Index.buildStructure threadIDs
return
threadsOnPage: (pageNum) ->
nodesPerPage = Index.threadsNumPerPage
offset = nodesPerPage * (pageNum - 1)
Index.sortedThreadIDs[offset ... offset + nodesPerPage]
buildStructure: (threadIDs) ->
threads = Index.buildThreads threadIDs, false, Conf['Show Replies']
nodes = []
for thread in threads
nodes.push thread.nodes.root, $.el('hr')
$.add Index.root, nodes
if Index.root.parentNode
$.event 'PostsInserted', null, Index.root
Index.loaded = true
return
buildCatalog: (threadIDs) ->
i = 0
n = threadIDs.length
node0 = null
fn = ->
return if node0 and !node0.parentNode # Index.root cleared
j = if i > 0 and Index.root.parentNode then n else i + 30
node0 = Index.buildCatalogPart(threadIDs[i...j])[0]
i = j
if i < n
$.queueTask fn
else
if Index.root.parentNode
$.event 'PostsInserted', null, Index.root
Index.loaded = true
fn()
return
buildCatalogPart: (threadIDs) ->
threads = Index.buildThreads threadIDs, true
Index.buildCatalogViews threads
Index.sizeCatalogViews threads
nodes = []
for thread in threads
thread.OP.setCatalogOP true
$.add thread.catalogView.nodes.root, thread.OP.nodes.root
nodes.push thread.catalogView.nodes.root
$.on thread.catalogView.nodes.root, 'mouseenter', Index.cb.catalogReplies.bind(thread)
$.on thread.OP.nodes.root, 'mouseenter', Index.cb.hoverAdjust.bind(thread.OP.nodes)
$.add Index.root, nodes
nodes
clearSearch: ->
Index.searchInput.value = ''
Index.onSearchInput()
Index.searchInput.focus()
setupSearch: ->
Index.searchInput.value = Index.search
if Index.search
Index.searchInput.dataset.searching = 1
else
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289
Index.searchInput.removeAttribute 'data-searching'
onSearchInput: ->
search = Index.searchInput.value.trim()
return if search is Index.search
Index.pushState
search: search
replace: !!search is !!Index.search
Index.pageLoad false
querySearch: (query) ->
if (match = query.match /^([\w+]+):\/(.*)\/(\w*)$/)
try
regexp = RegExp match[2], match[3]
catch
return []
return Index.sortedThreadIDs.filter (ID) ->
regexp.test(Filter.values(match[1], Index.parsedThreads[ID]).join('\n'))
return if not (keywords = query.toLowerCase().match /\S+/g)
Index.sortedThreadIDs.filter (ID) ->
Index.searchMatch Index.parsedThreads[ID], keywords
searchMatch: (obj, keywords) ->
{info, file} = obj
info.comment ?= g.SITE.Build.parseComment info.commentHTML.innerHTML
text = []
for key in ['comment', 'subject', 'name', 'tripcode']
text.push info[key] if key of info
text.push file.name if file
text = text.join(' ').toLowerCase()
for keyword in keywords
return false if -1 is text.indexOf keyword
return true