ThreadUpdater = init: -> 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 += "
" html = """ <%= grunt.file.read('html/Monitoring/ThreadUpdater.html').replace(/>\s+<').trim() %> """ @dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html @timer = $ '#update-timer', @dialog @status = $ '#update-status', @dialog @isUpdating = Conf['Auto Update'] Thread::callbacks.push name: 'Thread Updater' cb: @node node: -> ThreadUpdater.thread = @ ThreadUpdater.root = @OP.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' $.off input, 'change', $.cb.checked $.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' $.on input, 'click', ThreadUpdater.update $.on window, 'online offline', ThreadUpdater.cb.online $.on d, 'QRPostSuccessful', ThreadUpdater.cb.post $.on d, 'visibilitychange', ThreadUpdater.cb.visibility ThreadUpdater.cb.online() $.add d.body, ThreadUpdater.dialog beep: 'data:audio/wav;base64,<%= grunt.file.read("audio/beep.wav", {encoding: "base64"}) %>' cb: online: -> if ThreadUpdater.online = navigator.onLine ThreadUpdater.outdateCount = 0 ThreadUpdater.set 'timer', ThreadUpdater.getInterval() ThreadUpdater.update() if ThreadUpdater.isUpdating ThreadUpdater.set 'status', null, null else ThreadUpdater.set 'timer', null ThreadUpdater.set 'status', 'Offline', 'warning' ThreadUpdater.cb.autoUpdate() post: (e) -> return unless ThreadUpdater.isUpdating and e.detail.threadID is ThreadUpdater.thread.ID ThreadUpdater.outdateCount = 0 setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2 visibility: -> return if d.hidden # Reset the counter when we focus this tab. ThreadUpdater.outdateCount = 0 if ThreadUpdater.seconds > ThreadUpdater.interval ThreadUpdater.set 'timer', ThreadUpdater.getInterval() scrollBG: -> ThreadUpdater.scrollBG = if Conf['Scroll BG'] -> true else -> not d.hidden autoUpdate: (e) -> ThreadUpdater.isUpdating = @checked if e if ThreadUpdater.isUpdating and ThreadUpdater.online ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 else clearTimeout ThreadUpdater.timeoutID interval: (e) -> val = Math.max 5, parseInt @value, 10 ThreadUpdater.interval = @value = val $.cb.value.call @ if e load: -> {req} = ThreadUpdater switch req.status when 200 g.DEAD = false ThreadUpdater.parse JSON.parse(req.response).posts ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified' ThreadUpdater.set 'timer', ThreadUpdater.getInterval() when 404 g.DEAD = true ThreadUpdater.set 'timer', null ThreadUpdater.set 'status', '404', 'warning' clearTimeout ThreadUpdater.timeoutID ThreadUpdater.thread.kill() $.event 'ThreadUpdate', 404: true thread: ThreadUpdater.thread else ThreadUpdater.outdateCount++ ThreadUpdater.set 'timer', ThreadUpdater.getInterval() ### Status Code 304: Not modified 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. ### # XXX 304 -> 0 in Opera [text, klass] = if req.status in [0, 304] [null, null] else ["#{req.statusText} (#{req.status})", 'warning'] ThreadUpdater.set 'status', text, klass delete ThreadUpdater.req getInterval: -> i = ThreadUpdater.interval j = Math.min ThreadUpdater.outdateCount, 10 unless d.hidden # Lower the max refresh rate limit on visible tabs. j = Math.min j, 7 ThreadUpdater.seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] 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 klass isnt undefined timeout: -> ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 unless n = --ThreadUpdater.seconds ThreadUpdater.update() else if n <= -60 ThreadUpdater.set 'status', 'Retrying', null ThreadUpdater.update() else if n > 0 ThreadUpdater.set 'timer', n update: -> return unless ThreadUpdater.online ThreadUpdater.seconds = 0 ThreadUpdater.set 'timer', '...' if ThreadUpdater.req # abort() triggers onloadend, we don't want that. ThreadUpdater.req.onloadend = null ThreadUpdater.req.abort() url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json" ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load, headers: 'If-Modified-Since': ThreadUpdater.lastModified updateThreadStatus: (title, OP) -> titleLC = title.toLowerCase() return if ThreadUpdater.thread["is#{title}"] is !!OP[titleLC] unless ThreadUpdater.thread["is#{title}"] = !!OP[titleLC] message = if title is 'Sticky' 'The thread is not a sticky anymore.' else 'The thread is not closed anymore.' new Notification 'info', message, 30 $.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info return message = if title is 'Sticky' 'The thread is now a sticky.' else 'The thread is now closed.' new Notification 'info', message, 30 icon = $.el 'img', src: "//static.4chan.org/image/#{titleLC}.gif" alt: title title: title className: "#{titleLC}Icon" root = $ '[title="Quote this post"]', ThreadUpdater.thread.OP.nodes.info if title is 'Closed' root = $('.stickyIcon', ThreadUpdater.thread.OP.nodes.info) or root $.after root, [$.tn(' '), icon] parse: (postObjects) -> OP = postObjects[0] Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler ThreadUpdater.updateThreadStatus 'Sticky', OP ThreadUpdater.updateThreadStatus 'Closed', OP ThreadUpdater.thread.postLimit = !!OP.bumplimit ThreadUpdater.thread.fileLimit = !!OP.imagelimit nodes = [] # post container elements posts = [] # post objects index = [] # existing posts files = [] # existing files count = 0 # new posts count # Build the index, create posts. for postObject in postObjects num = postObject.no index.push num files.push num if postObject.fsize continue if num <= ThreadUpdater.lastPost # Insert new posts, not older ones. count++ node = Build.postFromObject postObject, ThreadUpdater.thread.board.ID nodes.push node posts.push new Post node, ThreadUpdater.thread, ThreadUpdater.thread.board deletedPosts = [] deletedFiles = [] # Check for deleted posts/files. for ID, post of ThreadUpdater.thread.posts # XXX tmp fix for 4chan's racing condition # giving us false-positive dead posts. # continue if post.isDead ID = +ID if post.isDead and ID in index post.resurrect() else unless ID in index post.kill() deletedPosts.push post else if post.file and !post.file.isDead and ID not in files post.kill true deletedFiles.push post unless count ThreadUpdater.set 'status', null, null ThreadUpdater.outdateCount++ else ThreadUpdater.set 'status', "+#{count}", 'new' ThreadUpdater.outdateCount = 0 if Conf['Beep'] and d.hidden and Unread.posts and !Unread.posts.length unless ThreadUpdater.audio ThreadUpdater.audio = $.el 'audio', src: ThreadUpdater.beep ThreadUpdater.audio.play() ThreadUpdater.lastPost = posts[count - 1].ID Main.callbackNodes Post, posts scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25 $.add ThreadUpdater.root, nodes if scroll if Conf['Bottom Scroll'] <% if (type === 'crx') { %>d.body<% } else { %>doc<% } %>.scrollTop = d.body.clientHeight else Header.scrollToPost nodes[0] $.queueTask -> # Enable 4chan features. threadID = ThreadUpdater.thread.ID {length} = $$ '.thread > .postContainer', ThreadUpdater.root if Conf['Enable 4chan\'s Extension'] $.globalEval "Parser.parseThread(#{threadID}, #{-count})" else Fourchan.parseThread threadID, length - count, length $.event 'ThreadUpdate', 404: false thread: ThreadUpdater.thread newPosts: posts deletedPosts: deletedPosts deletedFiles: deletedFiles postCount: OP.replies + 1 fileCount: OP.images + (!!ThreadUpdater.thread.OP.file and !ThreadUpdater.thread.OP.file.isDead)