368 lines
12 KiB
CoffeeScript
368 lines
12 KiB
CoffeeScript
ThreadUpdater =
|
|
init: ->
|
|
return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
|
|
|
|
if Conf['Updater and Stats in Header']
|
|
@dialog = sc = $.el 'span',
|
|
id: 'updater'
|
|
$.extend sc, <%= html('<span id="update-status" class="empty"></span><span id="update-timer" class="empty" title="Update now"></span>') %>
|
|
$.ready ->
|
|
Header.addShortcut sc
|
|
else
|
|
@dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;',
|
|
<%= html('<div class="move"></div><span id="update-status"></span><span id="update-timer" title="Update now"></span>') %>
|
|
$.addClass doc, 'float'
|
|
$.ready ->
|
|
$.add d.body, sc
|
|
|
|
@checkPostCount = 0
|
|
|
|
@timer = $ '#update-timer', sc
|
|
@status = $ '#update-status', sc
|
|
|
|
$.on @timer, 'click', @update
|
|
$.on @status, 'click', @update
|
|
|
|
updateLink = $.el 'span',
|
|
className: 'brackets-wrap updatelink'
|
|
$.extend updateLink, <%= html('<a href="javascript:;">Update</a>') %>
|
|
Main.ready ->
|
|
$.add navLinksBot, [$.tn(' '), updateLink] if (navLinksBot = $ '.navLinksBot')
|
|
$.on updateLink.firstElementChild, 'click', @update
|
|
|
|
subEntries = []
|
|
for name, conf of Config.updater.checkbox
|
|
el = UI.checkbox name, name
|
|
el.title = conf[1]
|
|
input = el.firstElementChild
|
|
$.on input, 'change', $.cb.checked
|
|
if input.name is 'Scroll BG'
|
|
$.on input, 'change', @cb.scrollBG
|
|
@cb.scrollBG()
|
|
else if input.name is 'Auto Update'
|
|
$.on input, 'change', @setInterval
|
|
subEntries.push el: el
|
|
|
|
@settings = $.el 'span',
|
|
<%= html('<a href="javascript:;">Interval</a>') %>
|
|
|
|
$.on @settings, 'click', @intervalShortcut
|
|
|
|
subEntries.push el: @settings
|
|
|
|
Header.menu.addEntry @entry =
|
|
el: $.el 'span',
|
|
textContent: 'Updater'
|
|
order: 110
|
|
subEntries: subEntries
|
|
|
|
Thread.callbacks.push
|
|
name: 'Thread Updater'
|
|
cb: @node
|
|
|
|
node: ->
|
|
ThreadUpdater.thread = @
|
|
ThreadUpdater.root = @OP.nodes.root.parentNode
|
|
ThreadUpdater.outdateCount = 0
|
|
|
|
# We must keep track of our own list of live posts/files
|
|
# to provide an accurate deletedPosts/deletedFiles on update
|
|
# as posts may be `kill`ed elsewhere.
|
|
ThreadUpdater.postIDs = []
|
|
ThreadUpdater.fileIDs = []
|
|
@posts.forEach (post) ->
|
|
ThreadUpdater.postIDs.push post.ID
|
|
ThreadUpdater.fileIDs.push post.ID if post.file
|
|
|
|
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
|
|
|
|
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost
|
|
$.on d, 'visibilitychange', ThreadUpdater.cb.visibility
|
|
|
|
ThreadUpdater.setInterval()
|
|
|
|
# Update immediately on /f/ to add files to replies.
|
|
ThreadUpdater.update() if @board.ID is 'f'
|
|
|
|
###
|
|
http://freesound.org/people/pierrecartoons1979/sounds/90112/
|
|
cc-by-nc-3.0
|
|
###
|
|
beep: 'data:audio/wav;base64,<%= grunt.file.read("src/Monitoring/beep.wav", {encoding: "base64"}) %>'
|
|
|
|
playBeep: ->
|
|
ThreadUpdater.audio or= $.el 'audio', src: ThreadUpdater.beep
|
|
{audio} = ThreadUpdater
|
|
if audio.paused
|
|
audio.play()
|
|
else
|
|
$.one audio, 'ended', ThreadUpdater.playBeep
|
|
|
|
cb:
|
|
checkpost: (e) ->
|
|
return if e.detail.threadID isnt ThreadUpdater.thread.ID
|
|
ThreadUpdater.postID = e.detail.postID
|
|
ThreadUpdater.checkPostCount = 0
|
|
ThreadUpdater.outdateCount = 0
|
|
ThreadUpdater.setInterval()
|
|
|
|
visibility: ->
|
|
return if d.hidden
|
|
# Reset the counter when we focus this tab.
|
|
ThreadUpdater.outdateCount = 0
|
|
if ThreadUpdater.seconds > ThreadUpdater.interval
|
|
ThreadUpdater.setInterval()
|
|
|
|
scrollBG: ->
|
|
ThreadUpdater.scrollBG = if Conf['Scroll BG']
|
|
-> true
|
|
else
|
|
-> not d.hidden
|
|
|
|
interval: (e) ->
|
|
val = parseInt @value, 10
|
|
if val < 1 then val = 1
|
|
ThreadUpdater.interval = @value = val
|
|
$.cb.value.call @ if e
|
|
|
|
load: ->
|
|
{req} = ThreadUpdater
|
|
switch req.status
|
|
when 200
|
|
ThreadUpdater.parse req
|
|
if ThreadUpdater.thread.isArchived
|
|
ThreadUpdater.kill()
|
|
else
|
|
ThreadUpdater.setInterval()
|
|
when 404
|
|
# XXX workaround for 4chan sending false 404s
|
|
$.ajax "//a.4cdn.org/#{ThreadUpdater.thread.board}/catalog.json", onloadend: ->
|
|
if @status is 200
|
|
confirmed = true
|
|
for page in @response
|
|
for thread in page.threads
|
|
if thread.no is ThreadUpdater.thread.ID
|
|
confirmed = false
|
|
break
|
|
else
|
|
confirmed = false
|
|
if confirmed
|
|
ThreadUpdater.kill()
|
|
else
|
|
ThreadUpdater.error req
|
|
else
|
|
ThreadUpdater.error req
|
|
|
|
kill: ->
|
|
ThreadUpdater.thread.kill()
|
|
ThreadUpdater.setInterval()
|
|
$.event 'ThreadUpdate',
|
|
404: true
|
|
threadID: ThreadUpdater.thread.fullID
|
|
|
|
error: (req) ->
|
|
if req.status is 304
|
|
ThreadUpdater.set 'status', ''
|
|
ThreadUpdater.setInterval()
|
|
unless req.status
|
|
ThreadUpdater.set 'status', 'Connection Failed', 'warning'
|
|
else if req.status isnt 304
|
|
ThreadUpdater.set 'status', "#{req.statusText} (#{req.status})", 'warning'
|
|
|
|
setInterval: ->
|
|
clearTimeout ThreadUpdater.timeoutID
|
|
|
|
if ThreadUpdater.thread.isDead
|
|
ThreadUpdater.set 'status', (if ThreadUpdater.thread.isArchived then 'Archived' else '404'), 'warning'
|
|
ThreadUpdater.set 'timer', ''
|
|
return
|
|
|
|
# Fetching your own posts after posting
|
|
if ThreadUpdater.postID and ThreadUpdater.checkPostCount < 5
|
|
ThreadUpdater.set 'timer', '...', 'loading'
|
|
ThreadUpdater.timeoutID = setTimeout ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND
|
|
return
|
|
|
|
unless Conf['Auto Update']
|
|
ThreadUpdater.set 'timer', 'Update'
|
|
return
|
|
|
|
{interval} = ThreadUpdater
|
|
if Conf['Optional Increase']
|
|
# Lower the max refresh rate limit on visible tabs.
|
|
limit = if d.hidden then 10 else 5
|
|
j = Math.min ThreadUpdater.outdateCount, limit
|
|
|
|
# 1 second to 100, 30 to 300.
|
|
cur = (Math.floor(interval * 0.1) or 1) * j * j
|
|
ThreadUpdater.seconds = $.minmax cur, interval, 300
|
|
else
|
|
ThreadUpdater.seconds = interval
|
|
|
|
ThreadUpdater.timeout()
|
|
|
|
intervalShortcut: ->
|
|
Settings.open 'Advanced'
|
|
settings = $.id 'fourchanx-settings'
|
|
$('input[name=Interval]', settings).focus()
|
|
|
|
set: (name, text, klass) ->
|
|
el = ThreadUpdater[name]
|
|
if node = el.firstChild
|
|
# Prevent the creation of a new DOM Node
|
|
# by setting the text node's data.
|
|
node.data = text
|
|
else
|
|
el.textContent = text
|
|
el.className = klass ? (if text is '' then 'empty' else '')
|
|
|
|
timeout: ->
|
|
if ThreadUpdater.seconds
|
|
ThreadUpdater.set 'timer', ThreadUpdater.seconds
|
|
ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000
|
|
else
|
|
ThreadUpdater.outdateCount++
|
|
ThreadUpdater.update()
|
|
ThreadUpdater.seconds--
|
|
|
|
update: ->
|
|
clearTimeout ThreadUpdater.timeoutID
|
|
ThreadUpdater.set 'timer', '...', 'loading'
|
|
ThreadUpdater.req?.abort()
|
|
ThreadUpdater.req = $.ajax "//a.4cdn.org/#{ThreadUpdater.thread.board}/thread/#{ThreadUpdater.thread}.json",
|
|
onloadend: ThreadUpdater.cb.load
|
|
timeout: $.MINUTE
|
|
,
|
|
whenModified: 'ThreadUpdater'
|
|
|
|
updateThreadStatus: (type, status) ->
|
|
return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
|
|
ThreadUpdater.thread.setStatus type, status
|
|
return if type is 'Closed' and ThreadUpdater.thread.isArchived
|
|
change = if type is 'Sticky'
|
|
if status
|
|
'now a sticky'
|
|
else
|
|
'not a sticky anymore'
|
|
else
|
|
if status
|
|
'now closed'
|
|
else
|
|
'not closed anymore'
|
|
new Notice 'info', "The thread is #{change}.", 30
|
|
|
|
parse: (req) ->
|
|
postObjects = req.response.posts
|
|
OP = postObjects[0]
|
|
{thread} = ThreadUpdater
|
|
{board} = thread
|
|
[..., lastPost] = ThreadUpdater.postIDs
|
|
|
|
# XXX Reject updates that falsely delete the last post.
|
|
return if postObjects[postObjects.length-1].no < lastPost and
|
|
new Date(req.getResponseHeader('Last-Modified')) - thread.posts[lastPost].info.date < 30 * $.SECOND
|
|
|
|
Build.spoilerRange[board] = OP.custom_spoiler
|
|
thread.setStatus 'Archived', !!OP.archived
|
|
ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky
|
|
ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed
|
|
thread.postLimit = !!OP.bumplimit
|
|
thread.fileLimit = !!OP.imagelimit
|
|
thread.ipCount = OP.unique_ips if OP.unique_ips?
|
|
|
|
posts = [] # new post objects
|
|
index = [] # existing posts
|
|
files = [] # existing files
|
|
newPosts = [] # new post fullID list for API
|
|
|
|
# Build the index, create posts.
|
|
for postObject in postObjects
|
|
ID = postObject.no
|
|
index.push ID
|
|
files.push ID if postObject.fsize
|
|
|
|
# Add files to replies on /f/.
|
|
if board.ID is 'f' and postObject.fsize and (post = thread.posts[ID]) and not post.file
|
|
node = Build.postFromObject postObject, board.ID
|
|
$.after post.nodes.info, $('.file', node)
|
|
post.parseFile()
|
|
Post.callbacks.execute post, ['Filter', 'File Info Formatting', 'Fappe Tyme', 'Sauce']
|
|
|
|
# Insert new posts, not older ones.
|
|
continue if ID <= lastPost
|
|
|
|
# XXX Resurrect wrongly deleted posts.
|
|
if (post = thread.posts[ID]) and not post.isFetchedQuote
|
|
post.resurrect()
|
|
continue
|
|
|
|
newPosts.push "#{board}.#{ID}"
|
|
node = Build.postFromObject postObject, board.ID
|
|
posts.push new Post node, thread, board
|
|
# Fetching your own posts after posting
|
|
delete ThreadUpdater.postID if ThreadUpdater.postID is ID
|
|
|
|
# Check for deleted posts.
|
|
deletedPosts = []
|
|
for ID in ThreadUpdater.postIDs when ID not in index
|
|
thread.posts[ID].kill()
|
|
deletedPosts.push "#{board}.#{ID}"
|
|
ThreadUpdater.postIDs = index
|
|
|
|
# Check for deleted files.
|
|
deletedFiles = []
|
|
for ID in ThreadUpdater.fileIDs when not (ID in files or "#{board}.#{ID}" in deletedPosts)
|
|
thread.posts[ID].kill true
|
|
deletedFiles.push "#{board}.#{ID}"
|
|
ThreadUpdater.fileIDs = files
|
|
|
|
unless posts.length
|
|
ThreadUpdater.set 'status', ''
|
|
else
|
|
ThreadUpdater.set 'status', "+#{posts.length}", 'new'
|
|
ThreadUpdater.outdateCount = 0
|
|
|
|
unreadCount = Unread.posts?.size
|
|
unreadQYCount = Unread.postsQuotingYou?.size
|
|
|
|
Main.callbackNodes Post, posts
|
|
|
|
if d.hidden or not d.hasFocus()
|
|
if Conf['Beep Quoting You'] and Unread.postsQuotingYou?.size > unreadQYCount
|
|
ThreadUpdater.playBeep()
|
|
ThreadUpdater.playBeep() if Conf['Beep']
|
|
else if Conf['Beep'] and Unread.posts?.size > 0 and unreadCount is 0
|
|
ThreadUpdater.playBeep()
|
|
|
|
scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and
|
|
ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
|
|
|
|
firstPost = null
|
|
for post in posts
|
|
unless QuoteThreading.insert post
|
|
firstPost or= post.nodes.root
|
|
$.add ThreadUpdater.root, post.nodes.root
|
|
$.event 'PostsInserted'
|
|
|
|
if scroll
|
|
if Conf['Bottom Scroll']
|
|
window.scrollTo 0, d.body.clientHeight
|
|
else
|
|
Header.scrollTo firstPost if firstPost
|
|
|
|
# Update IP count in original post form.
|
|
if OP.unique_ips? and ipCountEl = $.id('unique-ips')
|
|
ipCountEl.textContent = OP.unique_ips
|
|
ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, if OP.unique_ips is 1 then 'is' else 'are')
|
|
ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, if OP.unique_ips is 1 then 'poster' else 'posters')
|
|
|
|
$.event 'ThreadUpdate',
|
|
404: false
|
|
threadID: thread.fullID
|
|
newPosts: newPosts
|
|
deletedPosts: deletedPosts
|
|
deletedFiles: deletedFiles
|
|
postCount: OP.replies + 1
|
|
fileCount: OP.images + !!OP.fsize
|
|
ipCount: OP.unique_ips
|