ThreadUpdater = init: -> return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] # Chromium won't play audio created in an inactive tab until the tab has been focused, so set it up now. # XXX Sometimes the loading stalls in Firefox, esp. when opening in private browsing window followed by normal window. # Don't let it keep the loading icon on indefinitely. @audio = $.el 'audio' @audio.src = @beep unless $.engine is 'gecko' if Conf['Updater and Stats in Header'] @dialog = sc = $.el 'span', id: 'updater' $.extend sc, <%= html('') %> Header.addShortcut 'updater', sc, 100 else @dialog = sc = UI.dialog 'updater', <%= html('
') %> $.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('Update') %> 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('Interval') %> $.on @settings, 'click', @intervalShortcut subEntries.push el: @settings Header.menu.addEntry @entry = el: $.el 'span', textContent: 'Updater' order: 110 subEntries: subEntries Callbacks.Thread.push name: 'Thread Updater' cb: @node node: -> ThreadUpdater.thread = @ ThreadUpdater.root = @nodes.root 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() ### http://freesound.org/people/pierrecartoons1979/sounds/90112/ cc-by-nc-3.0 ### beep: 'data:audio/wav;base64,<%= readBase64('beep.wav') %>' playBeep: -> {audio} = ThreadUpdater audio.src or= ThreadUpdater.beep 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 "#{location.protocol}//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 Error', '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 "#{location.protocol}//a.4cdn.org/#{ThreadUpdater.thread.board}/thread/#{ThreadUpdater.thread}.json", onloadend: ThreadUpdater.cb.load timeout: $.MINUTE , whenModified: 'ThreadUpdater' updateThreadStatus: (type, status) -> return if not (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 # 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