unoop the thread updater.

This commit is contained in:
Nicolas Stepien 2013-02-14 21:31:31 +01:00
parent bfffdbee91
commit b90931b80d
5 changed files with 452 additions and 481 deletions

File diff suppressed because one or more lines are too long

View File

@ -178,12 +178,16 @@ a[href="javascript:;"] {
} }
/* Thread Updater */ /* Thread Updater */
#updater {
text-align: right;
}
#updater:not(:hover) { #updater:not(:hover) {
background: none; background: none;
border: none; border: none;
box-shadow: none;
}
#updater > .move {
padding: 0 3px;
}
#updater > div:last-child {
text-align: center;
} }
#updater input[type=number] { #updater input[type=number] {
width: 4em; width: 4em;

View File

@ -2313,234 +2313,221 @@ ThreadUpdater =
init: -> init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
html = ''
for name, conf of Config.updater.checkbox
checked = if Conf[name] then 'checked' else ''
html += "<div><label title='#{conf[1]}'><input name='#{name}' type=checkbox #{checked}> #{name}</label></div>"
checked = if Conf['Auto Update'] then 'checked' else ''
html = """
<div class=move><span id=update-status></span> <span id=update-timer></span></div>
#{html}
<div><label title='Controls whether *this* thread automatically updates or not'><input type=checkbox name='Auto Update This' #{checked}> Auto Update This</label></div>
<div><label><input type=number name=Interval class=field min=5 value=#{Conf['Interval']}> Refresh rate (s)</label></div>
<div><input value='Update Now' type=button name='Update Now'></div>
"""
@dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html
@timer = $ '#update-timer', @dialog
@status = $ '#update-status', @dialog
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Updater' name: 'Thread Updater'
cb: @node cb: @node
node: -> node: ->
new ThreadUpdater.Updater @ ThreadUpdater.thread = @
ThreadUpdater.root = @posts[@].nodes.root.parentNode
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]
ThreadUpdater.outdateCount = 0
ThreadUpdater.lastModified = '0'
for input in $$ 'input', ThreadUpdater.dialog
if input.type is 'checkbox'
$.on input, 'change', $.cb.checked
switch input.name
when 'Scroll BG'
$.on input, 'change', ThreadUpdater.cb.scrollBG
ThreadUpdater.cb.scrollBG()
when 'Auto Update This'
$.on input, 'change', ThreadUpdater.cb.autoUpdate
$.event 'change', null, input
when 'Interval'
$.on input, 'change', ThreadUpdater.cb.interval
ThreadUpdater.cb.interval.call input
when 'Update Now'
$.on input, 'click', ThreadUpdater.update
$.on window, 'online offline', ThreadUpdater.cb.online
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.post
$.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', ThreadUpdater.cb.visibility
ThreadUpdater.cb.online()
$.add d.body, ThreadUpdater.dialog
### ###
http://freesound.org/people/pierrecartoons1979/sounds/90112/ http://freesound.org/people/pierrecartoons1979/sounds/90112/
cc-by-nc-3.0 cc-by-nc-3.0
### ###
beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA' beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA'
Updater: class
constructor: (@thread) ->
html = '<div class=move><span id=status></span> <span id=timer></span></div>'
for name, val of Config.updater.checkbox
title = val[1]
checked = if Conf[name] then 'checked' else ''
html += "<div><label title='#{title}'>#{name}<input name='#{name}' type=checkbox #{checked}></label></div>"
checked = if Conf['Auto Update'] then 'checked' else ''
html += """
<div><label title='Controls whether *this* thread automatically updates or not'>Auto Update This<input name='Auto Update This' type=checkbox #{checked}></label></div>
<div><label>Interval (s)<input type=number name=Interval class=field min=5 value=#{Conf['Interval']}></label></div>
<div><input value='Update Now' type=button name='Update Now'></div>
"""
dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html
@timer = $ '#timer', dialog
@status = $ '#status', dialog
@unsuccessfulFetchCount = 0
@lastModified = '0'
@threadRoot = thread.posts[thread].nodes.root.parentNode
@lastPost = +@threadRoot.lastElementChild.id[2..]
for input in $$ 'input', dialog
if input.type is 'checkbox'
$.on input, 'click', @cb.checkbox.bind @
$.event 'click', null, input
switch input.name
when 'Scroll BG'
$.on input, 'click', @cb.scrollBG.bind @
@cb.scrollBG.call @
when 'Auto Update This'
$.on input, 'click', @cb.autoUpdate.bind @
when 'Interval'
$.on input, 'change', @cb.interval.bind @
$.event 'change', null, input
when 'Update Now'
$.on input, 'click', @update.bind @
$.on window, 'online offline', @cb.online.bind @
$.on d, 'QRPostSuccessful', @cb.post.bind @
$.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', @cb.visibility.bind @
@cb.online.call @
$.add d.body, dialog
cb: cb:
online: -> online: ->
if @online = navigator.onLine if ThreadUpdater.online = navigator.onLine
@unsuccessfulFetchCount = 0 ThreadUpdater.outdateCount = 0
@set 'timer', @getInterval() ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
@update() if Conf['Auto Update This'] ThreadUpdater.update() if Conf['Auto Update This']
@set 'status', null ThreadUpdater.set 'status', null, null
@status.className = null
else else
@status.className = 'warning' ThreadUpdater.set 'timer', null
@set 'status', 'Offline' ThreadUpdater.set 'status', 'Offline', 'warning'
@set 'timer', null ThreadUpdater.cb.autoUpdate()
@cb.autoUpdate.call @
post: (e) -> post: (e) ->
return unless @['Auto Update This'] and +e.detail.threadID is @thread.ID return unless Conf['Auto Update This'] and +e.detail.threadID is @thread.ID
@unsuccessfulFetchCount = 0 @outdateCount = 0
setTimeout @update.bind(@), 1000 if @seconds > 2 setTimeout @update.bind(@), 1000 if @seconds > 2
visibility: -> visibility: ->
return if $.hidden() return if $.hidden()
# Reset the counter when we focus this tab. # Reset the counter when we focus this tab.
@unsuccessfulFetchCount = 0 ThreadUpdater.outdateCount = 0
if @seconds > @interval if ThreadUpdater.seconds > ThreadUpdater.interval
@set 'timer', @getInterval() ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
checkbox: (e) ->
input = e.target
{checked, name} = input
@[name] = checked
$.cb.checked.call input
scrollBG: -> scrollBG: ->
@scrollBG = ThreadUpdater.scrollBG = if Conf['Scroll BG']
if @['Scroll BG']
-> true -> true
else else
-> not $.hidden() -> not $.hidden()
autoUpdate: -> autoUpdate: ->
if @['Auto Update This'] and @online if Conf['Auto Update This'] and ThreadUpdater.online
@timeoutID = setTimeout @timeout.bind(@), 1000 ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000
else else
clearTimeout @timeoutID clearTimeout ThreadUpdater.timeoutID
interval: (e) -> interval: ->
input = e.target val = Math.max 5, parseInt @value, 10
val = Math.max 5, parseInt input.value, 10 ThreadUpdater.interval = @value = val
@interval = input.value = val $.cb.value.call @
$.cb.value.call input
load: -> load: ->
switch @req.status {req} = ThreadUpdater
switch req.status
when 200
ThreadUpdater.parse JSON.parse(req.response).posts
ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified'
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
when 404 when 404
@set 'timer', null ThreadUpdater.set 'timer', null
@set 'status', '404' ThreadUpdater.set 'status', '404', 'warning'
@status.className = 'warning' clearTimeout ThreadUpdater.timeoutID
clearTimeout @timeoutID ThreadUpdater.thread.kill()
@thread.isDead = true
# if Conf['Unread Count'] # if Conf['Unread Count']
# Unread.title = Unread.title.match(/^.+-/)[0] + ' 404' # Unread.title = Unread.title.match(/^.+-/)[0] + ' 404'
# else # else
# d.title = d.title.match(/^.+-/)[0] + ' 404' # d.title = d.title.match(/^.+-/)[0] + ' 404'
# Unread.update true # Unread.update true
# QR.abort() # QR.abort()
# XXX 304 -> 0 in Opera ThreadUpdater.outdateCount++
when 0, 304 ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
else
ThreadUpdater.outdateCount++
ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
### ###
Status Code 304: Not modified Status Code 304: Not modified
By sending the `If-Modified-Since` header we get a proper status code, and no response. By sending the `If-Modified-Since` header we get a proper status code, and no response.
This saves bandwidth for both the user and the servers and avoid unnecessary computation. This saves bandwidth for both the user and the servers and avoid unnecessary computation.
### ###
@unsuccessfulFetchCount++ # XXX 304 -> 0 in Opera
@set 'timer', @getInterval() [text, klass] = if req.status in [0, 304]
@set 'status', null [null, null]
@status.className = null
when 200
@parse JSON.parse(@req.response).posts
@lastModified = @req.getResponseHeader 'Last-Modified'
@set 'timer', @getInterval()
else else
@unsuccessfulFetchCount++ ["#{req.statusText} (#{req.status})", 'warning']
@set 'timer', @getInterval() ThreadUpdater.set 'status', text, klass
@set 'status', "#{@req.statusText} (#{@req.status})" delete ThreadUpdater.req
@status.className = 'warning'
delete @req
getInterval: -> getInterval: ->
i = @interval i = ThreadUpdater.interval
j = Math.min @unsuccessfulFetchCount, 10 j = Math.min ThreadUpdater.outdateCount, 10
unless $.hidden() unless $.hidden()
# Lower the max refresh rate limit on visible tabs. # Lower the max refresh rate limit on visible tabs.
j = Math.min j, 7 j = Math.min j, 7
@seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] ThreadUpdater.seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]
set: (name, text) -> set: (name, text, klass) ->
el = @[name] el = ThreadUpdater[name]
if node = el.firstChild if node = el.firstChild
# Prevent the creation of a new DOM Node # Prevent the creation of a new DOM Node
# by setting the text node's data. # by setting the text node's data.
node.data = text node.data = text
else else
el.textContent = text el.textContent = text
el.className = klass if klass isnt undefined
timeout: -> timeout: ->
@timeoutID = setTimeout @timeout.bind(@), 1000 ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000
unless n = --@seconds unless n = --ThreadUpdater.seconds
@update() ThreadUpdater.update()
else if n <= -60 else if n <= -60
@set 'status', 'Retrying' ThreadUpdater.set 'status', 'Retrying', null
@status.className = null ThreadUpdater.update()
@update()
else if n > 0 else if n > 0
@set 'timer', n ThreadUpdater.set 'timer', n
update: -> update: ->
return unless @online return unless ThreadUpdater.online
@seconds = 0 ThreadUpdater.seconds = 0
@set 'timer', '...' ThreadUpdater.set 'timer', '...'
if @req if ThreadUpdater.req
# abort() triggers onloadend, we don't want that. # abort() triggers onloadend, we don't want that.
@req.onloadend = null ThreadUpdater.req.onloadend = null
@req.abort() ThreadUpdater.req.abort()
url = "//api.4chan.org/#{@thread.board}/res/#{@thread}.json" url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
@req = $.ajax url, onloadend: @cb.load.bind @, ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
headers: 'If-Modified-Since': @lastModified headers: 'If-Modified-Since': ThreadUpdater.lastModified
parse: (postObjects) -> parse: (postObjects) ->
Build.spoilerRange[@thread.board] = postObjects[0].custom_spoiler Build.spoilerRange[ThreadUpdater.thread.board] = postObjects[0].custom_spoiler
nodes = [] # post container elements nodes = [] # post container elements
posts = [] # post objects posts = [] # post objects
index = [] # existing posts index = [] # existing posts
image = [] # existing images files = [] # existing files
count = 0 # new posts count count = 0 # new posts count
# Build the index, create posts. # Build the index, create posts.
for postObject in postObjects for postObject in postObjects
num = postObject.no num = postObject.no
index.push num index.push num
image.push num if postObject.ext files.push num if postObject.fsize
continue if num <= @lastPost continue if num <= ThreadUpdater.lastPost
# Insert new posts, not older ones. # Insert new posts, not older ones.
count++ count++
node = Build.postFromObject postObject, @thread.board.ID node = Build.postFromObject postObject, ThreadUpdater.thread.board.ID
nodes.push node nodes.push node
posts.push new Post node, @thread, @thread.board posts.push new Post node, ThreadUpdater.thread, ThreadUpdater.thread.board
# Check for deleted posts and deleted images. # Check for deleted posts/files.
for i, post of @thread.posts for ID, post of ThreadUpdater.thread.posts
continue if post.isDead continue if post.isDead
{ID} = post ID = +ID
if -1 is index.indexOf ID if -1 is index.indexOf ID
post.kill() post.kill()
else if post.file and !post.file.isDead and -1 is image.indexOf ID else if post.file and !post.file.isDead and -1 is files.indexOf ID
post.kill true post.kill true
if count if count
if Conf['Beep'] and $.hidden() and (Unread.replies.length is 0) if Conf['Beep'] and $.hidden() #and !Unread.replies.length
unless @audio unless ThreadUpdater.audio
@audio = $.el 'audio', src: ThreadUpdater.beep ThreadUpdater.audio = $.el 'audio', src: ThreadUpdater.beep
audio.play() ThreadUpdater.audio.play()
@set 'status', "+#{count}" ThreadUpdater.set 'status', "+#{count}", 'new'
@status.className = 'new' ThreadUpdater.outdateCount = 0
@unsuccessfulFetchCount = 0
else else
@set 'status', null ThreadUpdater.set 'status', null, null
@status.className = null ThreadUpdater.outdateCount++
@unsuccessfulFetchCount++
return return
@lastPost = posts[count - 1].ID ThreadUpdater.lastPost = posts[count - 1].ID
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
scroll = @['Auto Scroll'] and @scrollBG() and scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and
@threadRoot.getBoundingClientRect().bottom - doc.clientHeight < 25 ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
$.add @threadRoot, nodes $.add ThreadUpdater.root, nodes
if scroll if scroll
nodes[0].scrollIntoView() nodes[0].scrollIntoView()

View File

@ -24,6 +24,10 @@ class Thread
g.threads["#{board}.#{@}"] = board.threads[@] = @ g.threads["#{board}.#{@}"] = board.threads[@] = @
kill: ->
@isDead = true
@timeOfDeath = Date.now()
class Post class Post
callbacks: [] callbacks: []
toString: -> @ID toString: -> @ID

View File

@ -611,7 +611,7 @@ QR =
# Create a custom event when the QR dialog is first initialized. # Create a custom event when the QR dialog is first initialized.
# Use it to extend the QR's functionalities, or for XTRM RICE. # Use it to extend the QR's functionalities, or for XTRM RICE.
$.event new CustomEvent 'QRDialogCreation', null, QR.el $.event 'QRDialogCreation', null, QR.el
submit: (e) -> submit: (e) ->
e?.preventDefault() e?.preventDefault()
@ -777,7 +777,7 @@ QR =
[_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/ [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
# Post/upload confirmed as successful. # Post/upload confirmed as successful.
$.event new CustomEvent 'QRPostSuccessful', { $.event 'QRPostSuccessful', {
threadID threadID
postID postID
}, QR.el }, QR.el