-
- """
- $.on $('.export', section), 'click', Settings.export
- $.on $('.import', section), 'click', Settings.import
- $.on $('input', section), 'change', Settings.onImport
-
- items = {}
- inputs = {}
- for key, obj of Config.main
- fs = $.el 'fieldset',
- innerHTML: ""
- for key, arr of obj
- description = arr[1]
- div = $.el 'div',
- innerHTML: ": #{description}"
- input = $ 'input', div
- $.on input, 'change', $.cb.checked
- items[key] = Conf[key]
- inputs[key] = input
- $.add fs, div
- $.add section, fs
-
- $.get items, (items) ->
- for key, val of items
- inputs[key].checked = val
- return
-
- div = $.el 'div',
- innerHTML: ": Clear manually-hidden threads and posts on all boards. Refresh the page to apply."
- button = $ 'button', div
- hiddenNum = 0
- $.get 'hiddenThreads', boards: {}, (item) ->
- for ID, board of item.hiddenThreads.boards
- for ID, thread of board
- hiddenNum++
- button.textContent = "Hidden: #{hiddenNum}"
- $.get 'hiddenPosts', boards: {}, (item) ->
- for ID, board of item.hiddenPosts.boards
- for ID, thread of board
- for ID, post of thread
- hiddenNum++
- button.textContent = "Hidden: #{hiddenNum}"
- $.on button, 'click', ->
- @textContent = 'Hidden: 0'
- $.get 'hiddenThreads', boards: {}, (item) ->
- for boardID of item.hiddenThreads.boards
- localStorage.removeItem "4chan-hide-t-#{boardID}"
- $.delete ['hiddenThreads', 'hiddenPosts']
- $.after $('input[name="Stubs"]', section).parentNode.parentNode, div
- export: (now, data) ->
- unless typeof now is 'number'
- now = Date.now()
- data =
- version: g.VERSION
- date: now
- Conf['WatchedThreads'] = {}
- for db in DataBoards
- Conf[db] = boards: {}
- # Make sure to export the most recent data.
- $.get Conf, (Conf) ->
- data.Conf = Conf
- Settings.export now, data
- return
- a = $.el 'a',
- className: 'warning'
- textContent: 'Save me!'
- download: "<%= meta.name %> v#{g.VERSION}-#{now}.json"
- href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
- target: '_blank'
- <% if (type === 'userscript') { %>
- # XXX Firefox won't let us download automatically.
- p = $ '.imp-exp-result', Settings.dialog
- $.rmAll p
- $.add p, a
- <% } else { %>
- a.click()
- <% } %>
- import: ->
- @nextElementSibling.click()
- onImport: ->
- return unless file = @files[0]
- output = @parentNode.nextElementSibling
- unless confirm 'Your current settings will be entirely overwritten, are you sure?'
- output.textContent = 'Import aborted.'
- return
- reader = new FileReader()
- reader.onload = (e) ->
- try
- data = JSON.parse e.target.result
- Settings.loadSettings data
- if confirm 'Import successful. Refresh now?'
- window.location.reload()
- catch err
- output.textContent = 'Import failed due to an error.'
- c.error err.stack
- reader.readAsText file
- loadSettings: (data) ->
- version = data.version.split '.'
- if version[0] is '2'
- data = Settings.convertSettings data,
- # General confs
- 'Disable 4chan\'s extension': ''
- 'Catalog Links': ''
- 'Reply Navigation': ''
- 'Show Stubs': 'Stubs'
- 'Image Auto-Gif': 'Auto-GIF'
- 'Expand From Current': ''
- 'Unread Favicon': 'Unread Tab Icon'
- 'Post in Title': 'Thread Excerpt'
- 'Auto Hide QR': ''
- 'Open Reply in New Tab': ''
- 'Remember QR size': ''
- 'Quote Inline': 'Quote Inlining'
- 'Quote Preview': 'Quote Previewing'
- 'Indicate OP quote': 'Mark OP Quotes'
- 'Indicate Cross-thread Quotes': 'Mark Cross-thread Quotes'
- # filter
- 'uniqueid': 'uniqueID'
- 'mod': 'capcode'
- 'country': 'flag'
- 'md5': 'MD5'
- # keybinds
- 'openEmptyQR': 'Open empty QR'
- 'openQR': 'Open QR'
- 'openOptions': 'Open settings'
- 'close': 'Close'
- 'spoiler': 'Spoiler tags'
- 'code': 'Code tags'
- 'submit': 'Submit QR'
- 'watch': 'Watch'
- 'update': 'Update'
- 'unreadCountTo0': ''
- 'expandAllImages': 'Expand images'
- 'expandImage': 'Expand image'
- 'zero': 'Front page'
- 'nextPage': 'Next page'
- 'previousPage': 'Previous page'
- 'nextThread': 'Next thread'
- 'previousThread': 'Previous thread'
- 'expandThread': 'Expand thread'
- 'openThreadTab': 'Open thread'
- 'openThread': 'Open thread tab'
- 'nextReply': 'Next reply'
- 'previousReply': 'Previous reply'
- 'hide': 'Hide'
- # updater
- 'Scrolling': 'Auto Scroll'
- 'Verbose': ''
- data.Conf.sauces = data.Conf.sauces.replace /\$\d/g, (c) ->
- switch c
- when '$1'
- '%TURL'
- when '$2'
- '%URL'
- when '$3'
- '%MD5'
- when '$4'
- '%board'
- else
- c
- for key, val of Config.hotkeys
- continue unless key of data.Conf
- data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) ->
- "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}"
- data.Conf.WatchedThreads = data.WatchedThreads
- $.set data.Conf
- convertSettings: (data, map) ->
- for prevKey, newKey of map
- data.Conf[newKey] = data.Conf[prevKey] if newKey
- delete data.Conf[prevKey]
- data
-
- filter: (section) ->
- section.innerHTML = """
-
-
- """
- select = $ 'select', section
- $.on select, 'change', Settings.selectFilter
- Settings.selectFilter.call select
- selectFilter: ->
- div = @nextElementSibling
- if (name = @value) isnt 'guide'
- $.rmAll div
- ta = $.el 'textarea',
- name: name
- className: 'field'
- spellcheck: false
- $.get name, Conf[name], (item) ->
- ta.value = item[name]
- $.on ta, 'change', $.cb.value
- $.add div, ta
- return
- div.innerHTML = """
-
Filter is disabled.
-
- Use regular expressions, one per line.
- Lines starting with a # will be ignored.
- For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.
- MD5 filtering uses exact string matching, not regular expressions.
-
-
You can use these settings with each regular expression, separate them with semicolons:
-
- Per boards, separate them with commas. It is global if not specified.
- For example: boards:a,jp;.
-
-
- Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).
- For example: op:only;, op:no; or op:yes;.
-
-
- Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).
- For example: stub:yes; or stub:no;.
-
-
- Highlight instead of hiding. You can specify a class name to use with a userstyle.
- For example: highlight; or highlight:wallpaper;.
-
-
- Highlighted OPs will have their threads put on top of board pages by default.
- For example: top:yes; or top:no;.
-
'
-
- for quote in $$ '.quotelink', container
- href = quote.getAttribute 'href'
- continue if href[0] is '/' # Cross-board quote, or board link
- quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
-
- container
-
-Get =
- threadExcerpt: (thread) ->
- {OP} = thread
- excerpt = OP.info.subject?.trim() or
- OP.info.comment.replace(/\n+/g, ' // ') or
- Conf['Anonymize'] and 'Anonymous' or
- $('.nameBlock', OP.nodes.info).textContent.trim()
- if excerpt.length > 70
- excerpt = "#{excerpt[...67]}..."
- "/#{thread.board}/ - #{excerpt}"
- postFromRoot: (root) ->
- link = $ 'a[title="Highlight this post"]', root
- boardID = link.pathname.split('/')[1]
- postID = link.hash[2..]
- index = root.dataset.clone
- post = g.posts["#{boardID}.#{postID}"]
- if index then post.clones[index] else post
- postFromNode: (root) ->
- Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', root
- contextFromLink: (quotelink) ->
- Get.postFromRoot $.x 'ancestor::div[parent::div[@class="thread"]][1]', quotelink
- postDataFromLink: (link) ->
- if link.hostname is 'boards.4chan.org'
- path = link.pathname.split '/'
- boardID = path[1]
- threadID = path[3]
- postID = link.hash[2..]
- else # resurrected quote
- boardID = link.dataset.boardid
- threadID = link.dataset.threadid or 0
- postID = link.dataset.postid
- return {
- boardID: boardID
- threadID: +threadID
- postID: +postID
- }
- allQuotelinksLinkingTo: (post) ->
- # Get quotelinks & backlinks linking to the given post.
- quotelinks = []
- # First:
- # In every posts,
- # if it did quote this post,
- # get all their backlinks.
- for ID, quoterPost of g.posts
- if post.fullID in quoterPost.quotes
- for quoterPost in [quoterPost].concat quoterPost.clones
- quotelinks.push.apply quotelinks, quoterPost.nodes.quotelinks
- # Second:
- # If we have quote backlinks:
- # in all posts this post quoted
- # and their clones,
- # get all of their backlinks.
- if Conf['Quote Backlinks']
- for quote in post.quotes
- continue unless quotedPost = g.posts[quote]
- for quotedPost in [quotedPost].concat quotedPost.clones
- quotelinks.push.apply quotelinks, [quotedPost.nodes.backlinks...]
- # Third:
- # Filter out irrelevant quotelinks.
- quotelinks.filter (quotelink) ->
- {boardID, postID} = Get.postDataFromLink quotelink
- boardID is post.board.ID and postID is post.ID
- postClone: (boardID, threadID, postID, root, context) ->
- if post = g.posts["#{boardID}.#{postID}"]
- Get.insert post, root, context
- return
-
- root.textContent = "Loading post No.#{postID}..."
- if threadID
- $.cache "//api.4chan.org/#{boardID}/res/#{threadID}.json", ->
- Get.fetchedPost @, boardID, threadID, postID, root, context
- else if url = Redirect.post boardID, postID
- $.cache url, ->
- Get.archivedPost @, boardID, postID, root, context
- insert: (post, root, context) ->
- # Stop here if the container has been removed while loading.
- return unless root.parentNode
- clone = post.addClone context
- Main.callbackNodes Post, [clone]
-
- # Get rid of the side arrows.
- {nodes} = clone
- $.rmAll nodes.root
- $.add nodes.root, nodes.post
-
- $.rmAll root
- $.add root, nodes.root
- fetchedPost: (req, boardID, threadID, postID, root, context) ->
- # In case of multiple callbacks for the same request,
- # don't parse the same original post more than once.
- if post = g.posts["#{boardID}.#{postID}"]
- Get.insert post, root, context
- return
-
- {status} = req
- if status not in [200, 304]
- # The thread can die by the time we check a quote.
- if url = Redirect.post boardID, postID
- $.cache url, ->
- Get.archivedPost @, boardID, postID, root, context
- else
- $.addClass root, 'warning'
- root.textContent =
- if status is 404
- "Thread No.#{threadID} 404'd."
- else
- "Error #{req.statusText} (#{req.status})."
- return
-
- posts = JSON.parse(req.response).posts
- Build.spoilerRange[boardID] = posts[0].custom_spoiler
- for post in posts
- break if post.no is postID # we found it!
- if post.no > postID
- # The post can be deleted by the time we check a quote.
- if url = Redirect.post boardID, postID
- $.cache url, ->
- Get.archivedPost @, boardID, postID, root, context
- else
- $.addClass root, 'warning'
- root.textContent = "Post No.#{postID} was not found."
- return
-
- board = g.boards[boardID] or
- new Board boardID
- thread = g.threads["#{boardID}.#{threadID}"] or
- new Thread threadID, board
- post = new Post Build.postFromObject(post, boardID), thread, board
- Main.callbackNodes Post, [post]
- Get.insert post, root, context
- archivedPost: (req, boardID, postID, root, context) ->
- # In case of multiple callbacks for the same request,
- # don't parse the same original post more than once.
- if post = g.posts["#{boardID}.#{postID}"]
- Get.insert post, root, context
- return
-
- data = JSON.parse req.response
- if data.error
- $.addClass root, 'warning'
- root.textContent = data.error
- return
-
- # convert comment to html
- bq = $.el 'blockquote', textContent: data.comment # set this first to convert text to HTML entities
- # https://github.com/eksopl/fuuka/blob/master/Board/Yotsuba.pm#L413-452
- # https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138
- bq.innerHTML = bq.innerHTML.replace ///
- \n
- | \[/?b\]
- | \[/?spoiler\]
- | \[/?code\]
- | \[/?moot\]
- | \[/?banned\]
- ///g, (text) ->
- switch text
- when '\n'
- ' '
- when '[b]'
- ''
- when '[/b]'
- ''
- when '[spoiler]'
- ''
- when '[/spoiler]'
- ''
- when '[code]'
- '
'
- when '[/code]'
- '
'
- when '[moot]'
- '
'
- when '[/moot]'
- '
'
- when '[banned]'
- ''
- when '[/banned]'
- ''
-
- comment = bq.innerHTML
- # greentext
- .replace(/(^|>)(>[^<$]*)(<|$)/g, '$1$2$3')
- # quotes
- .replace /((>){2}(>\/[a-z\d]+\/)?\d+)/g, '$1'
-
- threadID = data.thread_num
- o =
- # id
- postID: "#{postID}"
- threadID: "#{threadID}"
- boardID: boardID
- # info
- name: data.name_processed
- capcode: switch data.capcode
- when 'M' then 'mod'
- when 'A' then 'admin'
- when 'D' then 'developer'
- tripcode: data.trip
- uniqueID: data.poster_hash
- email: if data.email then encodeURI data.email else ''
- subject: data.title_processed
- flagCode: data.poster_country
- flagName: data.poster_country_name_processed
- date: data.fourchan_date
- dateUTC: data.timestamp
- comment: comment
- # file
- if data.media?.media_filename
- o.file =
- name: data.media.media_filename_processed
- timestamp: data.media.media_orig
- url: data.media.media_link or data.media.remote_media_link
- height: data.media.media_h
- width: data.media.media_w
- MD5: data.media.media_hash
- size: data.media.media_size
- turl: data.media.thumb_link or "//thumbs.4chan.org/#{boardID}/thumb/#{data.media.preview_orig}"
- theight: data.media.preview_h
- twidth: data.media.preview_w
- isSpoiler: data.media.spoiler is '1'
-
- board = g.boards[boardID] or
- new Board boardID
- thread = g.threads["#{boardID}.#{threadID}"] or
- new Thread threadID, board
- post = new Post Build.post(o, true), thread, board,
- isArchived: true
- Main.callbackNodes Post, [post]
- Get.insert post, root, context
-
-Quotify =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes']
-
- Post::callbacks.push
- name: 'Resurrect Quotes'
- cb: @node
- node: ->
- for deadlink in $$ '.deadlink', @nodes.comment
- if @isClone
- if $.hasClass deadlink, 'quotelink'
- @nodes.quotelinks.push deadlink
- else
- Quotify.parseDeadlink.call @, deadlink
- return
-
- parseDeadlink: (deadlink) ->
- if deadlink.parentNode.className is 'prettyprint'
- # Don't quotify deadlinks inside code tags,
- # un-`span` them.
- $.replace deadlink, [deadlink.childNodes...]
- return
-
- quote = deadlink.textContent
- return unless postID = quote.match(/\d+$/)?[0]
- boardID = if m = quote.match /^>>>\/([a-z\d]+)/
- m[1]
- else
- @board.ID
- quoteID = "#{boardID}.#{postID}"
-
- if post = g.posts[quoteID]
- unless post.isDead
- # Don't (Dead) when quotifying in an archived post,
- # and we know the post still exists.
- a = $.el 'a',
- href: "/#{boardID}/#{post.thread}/res/#p#{postID}"
- className: 'quotelink'
- textContent: quote
- else
- # Replace the .deadlink span if we can redirect.
- a = $.el 'a',
- href: "/#{boardID}/#{post.thread}/res/#p#{postID}"
- className: 'quotelink deadlink'
- target: '_blank'
- textContent: "#{quote}\u00A0(Dead)"
- a.setAttribute 'data-boardid', boardID
- a.setAttribute 'data-threadid', post.thread.ID
- a.setAttribute 'data-postid', postID
- else if redirect = Redirect.to {boardID, threadID: 0, postID}
- # Replace the .deadlink span if we can redirect.
- a = $.el 'a',
- href: redirect
- className: 'deadlink'
- target: '_blank'
- textContent: "#{quote}\u00A0(Dead)"
- if Redirect.post boardID, postID
- # Make it function as a normal quote if we can fetch the post.
- $.addClass a, 'quotelink'
- a.setAttribute 'data-boardid', boardID
- a.setAttribute 'data-postid', postID
-
- unless quoteID in @quotes
- @quotes.push quoteID
-
- unless a
- deadlink.textContent = "#{quote}\u00A0(Dead)"
- return
-
- $.replace deadlink, a
- if $.hasClass a, 'quotelink'
- @nodes.quotelinks.push a
-
-QuoteInline =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Quote Inlining']
-
- Post::callbacks.push
- name: 'Quote Inlining'
- cb: @node
- node: ->
- for link in @nodes.quotelinks.concat [@nodes.backlinks...]
- $.on link, 'click', QuoteInline.toggle
- return
- toggle: (e) ->
- return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
- e.preventDefault()
- {boardID, threadID, postID} = Get.postDataFromLink @
- context = Get.contextFromLink @
- if $.hasClass @, 'inlined'
- QuoteInline.rm @, boardID, threadID, postID, context
- else
- return if $.x "ancestor::div[@id='p#{postID}']", @
- QuoteInline.add @, boardID, threadID, postID, context
- @classList.toggle 'inlined'
-
- findRoot: (quotelink, isBacklink) ->
- if isBacklink
- quotelink.parentNode.parentNode
- else
- $.x 'ancestor-or-self::*[parent::blockquote][1]', quotelink
- add: (quotelink, boardID, threadID, postID, context) ->
- isBacklink = $.hasClass quotelink, 'backlink'
- inline = $.el 'div',
- id: "i#{postID}"
- className: 'inline'
- $.after QuoteInline.findRoot(quotelink, isBacklink), inline
- Get.postClone boardID, threadID, postID, inline, context
-
- return unless (post = g.posts["#{boardID}.#{postID}"]) and
- context.thread is post.thread
-
- # Hide forward post if it's a backlink of a post in this thread.
- # Will only unhide if there's no inlined backlinks of it anymore.
- if isBacklink and Conf['Forward Hiding']
- $.addClass post.nodes.root, 'forwarded'
- post.forwarded++ or post.forwarded = 1
-
- # Decrease the unread count if this post
- # is in the array of unread posts.
- return unless Unread.posts
- Unread.readSinglePost post
-
- rm: (quotelink, boardID, threadID, postID, context) ->
- isBacklink = $.hasClass quotelink, 'backlink'
- # Select the corresponding inlined quote, and remove it.
- root = QuoteInline.findRoot quotelink, isBacklink
- root = $.x "following-sibling::div[@id='i#{postID}'][1]", root
- $.rm root
-
- # Stop if it only contains text.
- return unless el = root.firstElementChild
-
- # Dereference clone.
- post = g.posts["#{boardID}.#{postID}"]
- post.rmClone el.dataset.clone
-
- # Decrease forward count and unhide.
- if Conf['Forward Hiding'] and
- isBacklink and
- context.thread is g.threads["#{boardID}.#{threadID}"] and
- not --post.forwarded
- delete post.forwarded
- $.rmClass post.nodes.root, 'forwarded'
-
- # Repeat.
- while inlined = $ '.inlined', el
- {boardID, threadID, postID} = Get.postDataFromLink inlined
- QuoteInline.rm inlined, boardID, threadID, postID, context
- $.rmClass inlined, 'inlined'
- return
-
-QuotePreview =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Quote Previewing']
-
- Post::callbacks.push
- name: 'Quote Previewing'
- cb: @node
- node: ->
- for link in @nodes.quotelinks.concat [@nodes.backlinks...]
- $.on link, 'mouseover', QuotePreview.mouseover
- return
- mouseover: (e) ->
- return if $.hasClass @, 'inlined'
-
- {boardID, threadID, postID} = Get.postDataFromLink @
-
- qp = $.el 'div',
- id: 'qp'
- className: 'dialog'
- $.add d.body, qp
- Get.postClone boardID, threadID, postID, qp, Get.contextFromLink @
-
- UI.hover
- root: @
- el: qp
- latestEvent: e
- endEvents: 'mouseout click'
- cb: QuotePreview.mouseout
- asapTest: -> qp.firstElementChild
-
- return unless origin = g.posts["#{boardID}.#{postID}"]
-
- if Conf['Quote Highlighting']
- posts = [origin].concat origin.clones
- # Remove the clone that's in the qp from the array.
- posts.pop()
- for post in posts
- $.addClass post.nodes.post, 'qphl'
-
- quoterID = $.x('ancestor::*[@id][1]', @).id.match(/\d+$/)[0]
- clone = Get.postFromRoot qp.firstChild
- for quote in clone.nodes.quotelinks.concat [clone.nodes.backlinks...]
- if quote.hash[2..] is quoterID
- $.addClass quote, 'forwardlink'
- return
- mouseout: ->
- # Stop if it only contains text.
- return unless root = @el.firstElementChild
-
- clone = Get.postFromRoot root
- post = clone.origin
- post.rmClone root.dataset.clone
-
- return unless Conf['Quote Highlighting']
- for post in [post].concat post.clones
- $.rmClass post.nodes.post, 'qphl'
- return
-
-QuoteBacklink =
- # Backlinks appending need to work for:
- # - previous, same, and following posts.
- # - existing and yet-to-exist posts.
- # - newly fetched posts.
- # - in copies.
- # XXX what about order for fetched posts?
- #
- # First callback creates backlinks and add them to relevant containers.
- # Second callback adds relevant containers into posts.
- # This is is so that fetched posts can get their backlinks,
- # and that as much backlinks are appended in the background as possible.
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Quote Backlinks']
-
- format = Conf['backlink'].replace /%id/g, "' + id + '"
- @funk = Function 'id', "return '#{format}'"
- @containers = {}
- Post::callbacks.push
- name: 'Quote Backlinking Part 1'
- cb: @firstNode
- Post::callbacks.push
- name: 'Quote Backlinking Part 2'
- cb: @secondNode
- firstNode: ->
- return if @isClone or !@quotes.length
- a = $.el 'a',
- href: "/#{@board}/res/#{@thread}#p#{@}"
- className: if @isHidden then 'filtered backlink' else 'backlink'
- textContent: QuoteBacklink.funk @ID
- for quote in @quotes
- containers = [QuoteBacklink.getContainer quote]
- if (post = g.posts[quote]) and post.nodes.backlinkContainer
- # Don't add OP clones when OP Backlinks is disabled,
- # as the clones won't have the backlink containers.
- for clone in post.clones
- containers.push clone.nodes.backlinkContainer
- for container in containers
- link = a.cloneNode true
- if Conf['Quote Previewing']
- $.on link, 'mouseover', QuotePreview.mouseover
- if Conf['Quote Inlining']
- $.on link, 'click', QuoteInline.toggle
- $.add container, [$.tn(' '), link]
- return
- secondNode: ->
- if @isClone and (@origin.isReply or Conf['OP Backlinks'])
- @nodes.backlinkContainer = $ '.container', @nodes.info
- return
- # Don't backlink the OP.
- return unless @isReply or Conf['OP Backlinks']
- container = QuoteBacklink.getContainer @fullID
- @nodes.backlinkContainer = container
- $.add @nodes.info, container
- getContainer: (id) ->
- @containers[id] or=
- $.el 'span', className: 'container'
-
-QuoteYou =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Mark Quotes of You'] or !Conf['Quick Reply']
-
- # \u00A0 is nbsp
- @text = '\u00A0(You)'
- Post::callbacks.push
- name: 'Mark Quotes of You'
- cb: @node
- node: ->
- # Stop there if it's a clone.
- return if @isClone
- # Stop there if there's no quotes in that post.
- return unless (quotes = @quotes).length
- {quotelinks} = @nodes
-
- for quotelink in quotelinks
- if QR.db.get Get.postDataFromLink quotelink
- $.add quotelink, $.tn QuoteYou.text
- return
-
-QuoteOP =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Mark OP Quotes']
-
- # \u00A0 is nbsp
- @text = '\u00A0(OP)'
- Post::callbacks.push
- name: 'Mark OP Quotes'
- cb: @node
- node: ->
- # Stop there if it's a clone of a post in the same thread.
- return if @isClone and @thread is @context.thread
- # Stop there if there's no quotes in that post.
- return unless (quotes = @quotes).length
- {quotelinks} = @nodes
-
- # rm (OP) from cross-thread quotes.
- if @isClone and @thread.fullID in quotes
- for quotelink in quotelinks
- quotelink.textContent = quotelink.textContent.replace QuoteOP.text, ''
-
- op = (if @isClone then @context else @).thread.fullID
- # add (OP) to quotes quoting this context's OP.
- return unless op in quotes
- for quotelink in quotelinks
- {boardID, postID} = Get.postDataFromLink quotelink
- if "#{boardID}.#{postID}" is op
- $.add quotelink, $.tn QuoteOP.text
- return
-
-QuoteCT =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Mark Cross-thread Quotes']
-
- # \u00A0 is nbsp
- @text = '\u00A0(Cross-thread)'
- Post::callbacks.push
- name: 'Mark Cross-thread Quotes'
- cb: @node
- node: ->
- # Stop there if it's a clone of a post in the same thread.
- return if @isClone and @thread is @context.thread
- # Stop there if there's no quotes in that post.
- return unless (quotes = @quotes).length
- {quotelinks} = @nodes
-
- {board, thread} = if @isClone then @context else @
- for quotelink in quotelinks
- {boardID, threadID} = Get.postDataFromLink quotelink
- continue unless threadID # deadlink
- if @isClone
- quotelink.textContent = quotelink.textContent.replace QuoteCT.text, ''
- if boardID is @board.ID and threadID isnt thread.ID
- $.add quotelink, $.tn QuoteCT.text
- return
-
-Anonymize =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Anonymize']
-
- Post::callbacks.push
- name: 'Anonymize'
- cb: @node
- node: ->
- return if @info.capcode or @isClone
- {name, tripcode, email} = @nodes
- if @info.name isnt 'Anonymous'
- name.textContent = 'Anonymous'
- if tripcode
- $.rm tripcode
- delete @nodes.tripcode
- if @info.email
- if /sage/i.test @info.email
- email.href = 'mailto:sage'
- else
- $.replace email, name
- delete @nodes.email
-
-Time =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Time Formatting']
-
- @funk = @createFunc Conf['time']
- Post::callbacks.push
- name: 'Time Formatting'
- cb: @node
- node: ->
- return if @isClone
- @nodes.date.textContent = Time.funk Time, @info.date
- createFunc: (format) ->
- code = format.replace /%([A-Za-z])/g, (s, c) ->
- if c of Time.formatters
- "' + Time.formatters.#{c}.call(date) + '"
- else
- s
- Function 'Time', 'date', "return '#{code}'"
- day: [
- 'Sunday'
- 'Monday'
- 'Tuesday'
- 'Wednesday'
- 'Thursday'
- 'Friday'
- 'Saturday'
- ]
- month: [
- 'January'
- 'February'
- 'March'
- 'April'
- 'May'
- 'June'
- 'July'
- 'August'
- 'September'
- 'October'
- 'November'
- 'December'
- ]
- zeroPad: (n) -> if n < 10 then "0#{n}" else n
- formatters:
- a: -> Time.day[@getDay()][...3]
- A: -> Time.day[@getDay()]
- b: -> Time.month[@getMonth()][...3]
- B: -> Time.month[@getMonth()]
- d: -> Time.zeroPad @getDate()
- e: -> @getDate()
- H: -> Time.zeroPad @getHours()
- I: -> Time.zeroPad @getHours() % 12 or 12
- k: -> @getHours()
- l: -> @getHours() % 12 or 12
- m: -> Time.zeroPad @getMonth() + 1
- M: -> Time.zeroPad @getMinutes()
- p: -> if @getHours() < 12 then 'AM' else 'PM'
- P: -> if @getHours() < 12 then 'am' else 'pm'
- S: -> Time.zeroPad @getSeconds()
- y: -> @getFullYear() - 2000
-
-RelativeDates =
- INTERVAL: $.MINUTE / 2
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Relative Post Dates']
-
- # Flush when page becomes visible again or when the thread updates.
- $.on d, 'visibilitychange ThreadUpdate', @flush
-
- # Start the timeout.
- @flush()
-
- Post::callbacks.push
- name: 'Relative Post Dates'
- cb: @node
- node: ->
- return if @isClone
-
- # Show original absolute time as tooltip so users can still know exact times
- # Since "Time Formatting" runs its `node` before us, the title tooltip will
- # pick up the user-formatted time instead of 4chan time when enabled.
- dateEl = @nodes.date
- dateEl.title = dateEl.textContent
-
- RelativeDates.setUpdate @
-
- # diff is milliseconds from now.
- relative: (diff, now, date) ->
- unit = if (number = (diff / $.DAY)) >= 1
- years = now.getYear() - date.getYear()
- months = now.getMonth() - date.getMonth()
- days = now.getDate() - date.getDate()
- if years > 1
- number = years - (months < 0 or months is 0 and days < 0)
- 'year'
- else if years is 1 and (months > 0 or months is 0 and days >= 0)
- number = years
- 'year'
- else if (months = (months+12)%12 ) > 1
- number = months - (days < 0)
- 'month'
- else if months is 1 and days >= 0
- number = months
- 'month'
- else
- 'day'
- else if (number = (diff / $.HOUR)) >= 1
- 'hour'
- else if (number = (diff / $.MINUTE)) >= 1
- 'minute'
- else
- # prevent "-1 seconds ago"
- number = Math.max(0, diff) / $.SECOND
- 'second'
-
- rounded = Math.round number
- unit += 's' if rounded isnt 1 # pluralize
-
- "#{rounded} #{unit} ago"
-
- # Changing all relative dates as soon as possible incurs many annoying
- # redraws and scroll stuttering. Thus, sacrifice accuracy for UX/CPU economy,
- # and perform redraws when the DOM is otherwise being manipulated (and scroll
- # stuttering won't be noticed), falling back to INTERVAL while the page
- # is visible.
- #
- # Each individual dateTime element will add its update() function to the stale list
- # when it is to be called.
- stale: []
- flush: ->
- # No point in changing the dates until the user sees them.
- return if d.hidden
-
- now = new Date()
- update now for update in RelativeDates.stale
- RelativeDates.stale = []
-
- # Reset automatic flush.
- clearTimeout RelativeDates.timeout
- RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL
-
- # Create function `update()`, closed over post, that, when called
- # from `flush()`, updates the elements, and re-calls `setOwnTimeout()` to
- # re-add `update()` to the stale list later.
- setUpdate: (post) ->
- setOwnTimeout = (diff) ->
- delay = if diff < $.MINUTE
- $.SECOND - (diff + $.SECOND / 2) % $.SECOND
- else if diff < $.HOUR
- $.MINUTE - (diff + $.MINUTE / 2) % $.MINUTE
- else if diff < $.DAY
- $.HOUR - (diff + $.HOUR / 2) % $.HOUR
- else
- $.DAY - (diff + $.DAY / 2) % $.DAY
- setTimeout markStale, delay
-
- update = (now) ->
- {date} = post.info
- diff = now - date
- relative = RelativeDates.relative diff, now, date
- for singlePost in [post].concat post.clones
- singlePost.nodes.date.firstChild.textContent = relative
- setOwnTimeout diff
-
- markStale = -> RelativeDates.stale.push update
-
- # Kick off initial timeout.
- update new Date()
-
-FileInfo =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['File Info Formatting']
-
- @funk = @createFunc Conf['fileInfo']
- Post::callbacks.push
- name: 'File Info Formatting'
- cb: @node
- node: ->
- return if !@file or @isClone
- @file.text.innerHTML = FileInfo.funk FileInfo, @
- createFunc: (format) ->
- code = format.replace /%(.)/g, (s, c) ->
- if c of FileInfo.formatters
- "' + FileInfo.formatters.#{c}.call(post) + '"
- else
- s
- Function 'FileInfo', 'post', "return '#{code}'"
- convertUnit: (size, unit) ->
- if unit is 'B'
- return "#{size.toFixed()} Bytes"
- i = 1 + ['KB', 'MB'].indexOf unit
- size /= 1024 while i--
- size =
- if unit is 'MB'
- Math.round(size * 100) / 100
- else
- size.toFixed()
- "#{size} #{unit}"
- escape: (name) ->
- name.replace /<|>/g, (c) ->
- c is '<' and '<' or '>'
- formatters:
- t: -> @file.URL.match(/\d+\..+$/)[0]
- T: -> "#{FileInfo.formatters.t.call @}"
- l: -> "#{FileInfo.formatters.n.call @}"
- L: -> "#{FileInfo.formatters.N.call @}"
- n: ->
- fullname = @file.name
- shortname = Build.shortFilename @file.name, @isReply
- if fullname is shortname
- FileInfo.escape fullname
- else
- "#{FileInfo.escape shortname}#{FileInfo.escape fullname}"
- N: -> FileInfo.escape @file.name
- p: -> if @file.isSpoiler then 'Spoiler, ' else ''
- s: -> @file.size
- B: -> FileInfo.convertUnit @file.sizeInBytes, 'B'
- K: -> FileInfo.convertUnit @file.sizeInBytes, 'KB'
- M: -> FileInfo.convertUnit @file.sizeInBytes, 'MB'
- r: -> if @file.isImage then @file.dimensions else 'PDF'
-
-Sauce =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Sauce']
-
- links = []
- for link in Conf['sauces'].split '\n'
- continue if link[0] is '#'
- links.push @createSauceLink link.trim()
- return unless links.length
- @links = links
- @link = $.el 'a', target: '_blank'
- Post::callbacks.push
- name: 'Sauce'
- cb: @node
- createSauceLink: (link) ->
- link = link.replace /%(T?URL|MD5|board)/g, (parameter) ->
- switch parameter
- when '%TURL'
- "' + encodeURIComponent(post.file.thumbURL) + '"
- when '%URL'
- "' + encodeURIComponent(post.file.URL) + '"
- when '%MD5'
- "' + encodeURIComponent(post.file.MD5) + '"
- when '%board'
- "' + encodeURIComponent(post.board) + '"
- else
- parameter
- text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1]
- link = link.replace /;text:.+$/, ''
- Function 'post', 'a', """
- a.href = '#{link}';
- a.textContent = '#{text}';
- return a;
- """
- node: ->
- return if @isClone or !@file
- nodes = []
- for link in Sauce.links
- # \u00A0 is nbsp
- nodes.push $.tn('\u00A0'), link @, Sauce.link.cloneNode true
- $.add @file.info, nodes
-
-ImageExpand =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Image Expansion']
-
- @EAI = $.el 'a',
- className: 'expand-all-shortcut'
- textContent: 'EAI'
- title: 'Expand All Images'
- href: 'javascript:;'
- $.on @EAI, 'click', ImageExpand.cb.toggleAll
- Header.addShortcut @EAI
-
- Post::callbacks.push
- name: 'Image Expansion'
- cb: @node
- node: ->
- return unless @file?.isImage
- {thumb} = @file
- $.on thumb.parentNode, 'click', ImageExpand.cb.toggle
- if @isClone and $.hasClass thumb, 'expanding'
- # If we clone a post where the image is still loading,
- # make it loading in the clone too.
- ImageExpand.contract @
- ImageExpand.expand @
- return
- if ImageExpand.on and !@isHidden
- ImageExpand.expand @
- cb:
- toggle: (e) ->
- return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
- e.preventDefault()
- ImageExpand.toggle Get.postFromNode @
- toggleAll: ->
- $.event 'CloseMenu'
- if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut'
- ImageExpand.EAI.className = 'contract-all-shortcut'
- ImageExpand.EAI.title = 'Contract All Images'
- func = ImageExpand.expand
- else
- ImageExpand.EAI.className = 'expand-all-shortcut'
- ImageExpand.EAI.title = 'Expand All Images'
- func = ImageExpand.contract
- for ID, post of g.posts
- for post in [post].concat post.clones
- {file} = post
- continue unless file and file.isImage and doc.contains post.nodes.root
- if ImageExpand.on and
- (!Conf['Expand spoilers'] and file.isSpoiler or
- Conf['Expand from here'] and file.thumb.getBoundingClientRect().top < 0)
- continue
- $.queueTask func, post
- return
- setFitness: ->
- {checked} = @
- (if checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-'
- return unless @name is 'Fit height'
- if checked
- $.on window, 'resize', ImageExpand.resize
- unless ImageExpand.style
- ImageExpand.style = $.addStyle null
- ImageExpand.resize()
- else
- $.off window, 'resize', ImageExpand.resize
-
- toggle: (post) ->
- {thumb} = post.file
- unless post.file.isExpanded or $.hasClass thumb, 'expanding'
- ImageExpand.expand post
- return
- ImageExpand.contract post
- rect = post.nodes.root.getBoundingClientRect()
- return unless rect.top <= 0 or rect.left <= 0
- # Scroll back to the thumbnail when contracting the image
- # to avoid being left miles away from the relevant post.
- {top} = rect
- unless Conf['Bottom header']
- headRect = Header.toggle.getBoundingClientRect()
- top += - headRect.top - headRect.height
- root = <% if (type === 'crx') { %>d.body<% } else { %>doc<% } %>
- root.scrollTop += top if rect.top < 0
- root.scrollLeft = 0 if rect.left < 0
-
- contract: (post) ->
- $.rmClass post.nodes.root, 'expanded-image'
- $.rmClass post.file.thumb, 'expanding'
- post.file.isExpanded = false
-
- expand: (post, src) ->
- # Do not expand images of hidden/filtered replies, or already expanded pictures.
- {thumb} = post.file
- return if post.isHidden or post.file.isExpanded or $.hasClass thumb, 'expanding'
- $.addClass thumb, 'expanding'
- if post.file.fullImage
- # Expand already-loaded/ing picture.
- $.asap (-> post.file.fullImage.naturalHeight), ->
- ImageExpand.completeExpand post
- return
- post.file.fullImage = img = $.el 'img',
- className: 'full-image'
- src: src or post.file.URL
- $.on img, 'error', ImageExpand.error
- $.asap (-> post.file.fullImage.naturalHeight), ->
- ImageExpand.completeExpand post
- $.after thumb, img
-
- completeExpand: (post) ->
- {thumb} = post.file
- return unless $.hasClass thumb, 'expanding' # contracted before the image loaded
- post.file.isExpanded = true
- unless post.nodes.root.parentNode
- # Image might start/finish loading before the post is inserted.
- # Don't scroll when it's expanded in a QP for example.
- $.addClass post.nodes.root, 'expanded-image'
- $.rmClass post.file.thumb, 'expanding'
- return
- prev = post.nodes.root.getBoundingClientRect()
- $.queueTask ->
- $.addClass post.nodes.root, 'expanded-image'
- $.rmClass post.file.thumb, 'expanding'
- return unless prev.top + prev.height <= 0
- root = <% if (type === 'crx') { %>d.body<% } else { %>doc<% } %>
- curr = post.nodes.root.getBoundingClientRect()
- root.scrollTop += curr.height - prev.height + curr.top - prev.top
-
- error: ->
- post = Get.postFromNode @
- $.rm @
- delete post.file.fullImage
- # Images can error:
- # - before the image started loading.
- # - after the image started loading.
- unless $.hasClass(post.file.thumb, 'expanding') or $.hasClass post.nodes.root, 'expanded-image'
- # Don't try to re-expend if it was already contracted.
- return
- ImageExpand.contract post
-
- src = @src.split '/'
- if src[2] is 'images.4chan.org'
- if URL = Redirect.image src[3], src[5]
- setTimeout ImageExpand.expand, 10000, post, URL
- return
- if g.DEAD or post.isDead or post.file.isDead
- return
-
- timeoutID = setTimeout ImageExpand.expand, 10000, post
- # XXX CORS for images.4chan.org WHEN?
- $.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: ->
- return if @status isnt 200
- for postObj in JSON.parse(@response).posts
- break if postObj.no is post.ID
- if postObj.no isnt post.ID
- clearTimeout timeoutID
- post.kill()
- else if postObj.filedeleted
- clearTimeout timeoutID
- post.kill true
-
- menu:
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Image Expansion']
-
- el = $.el 'span',
- textContent: 'Image Expansion'
- className: 'image-expansion-link'
-
- {createSubEntry} = ImageExpand.menu
- subEntries = []
- for key, conf of Config.imageExpansion
- subEntries.push createSubEntry key, conf
-
- $.event 'AddMenuEntry',
- type: 'header'
- el: el
- order: 80
- subEntries: subEntries
-
- createSubEntry: (type, config) ->
- label = $.el 'label',
- innerHTML: " #{type}"
- input = label.firstElementChild
- if type in ['Fit width', 'Fit height']
- $.on input, 'change', ImageExpand.cb.setFitness
- if config
- label.title = config[1]
- input.checked = Conf[type]
- $.event 'change', null, input
- $.on input, 'change', $.cb.checked
- el: label
-
- resize: ->
- ImageExpand.style.textContent = ":root.fit-height .full-image {max-height:#{doc.clientHeight}px}"
-
-RevealSpoilers =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Reveal Spoilers']
-
- Post::callbacks.push
- name: 'Reveal Spoilers'
- cb: @node
- node: ->
- return if @isClone or !@file?.isSpoiler
- {thumb} = @file
- thumb.removeAttribute 'style'
- thumb.src = @file.thumbURL
-
-AutoGIF =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Auto-GIF'] or g.BOARD.ID in ['gif', 'wsg']
-
- Post::callbacks.push
- name: 'Auto-GIF'
- cb: @node
- node: ->
- return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
- {thumb, URL} = @file
- return unless /gif$/.test(URL) and !/spoiler/.test thumb.src
- if @file.isSpoiler
- # Revealed spoilers do not have height/width set, this fixes auto-gifs dimensions.
- {style} = thumb
- style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px'
- gif = $.el 'img'
- $.on gif, 'load', ->
- # Replace the thumbnail once the GIF has finished loading.
- thumb.src = URL
- gif.src = URL
-
-ImageHover =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Image Hover']
-
- Post::callbacks.push
- name: 'Image Hover'
- cb: @node
- node: ->
- return unless @file?.isImage
- $.on @file.thumb, 'mouseover', ImageHover.mouseover
- mouseover: (e) ->
- post = Get.postFromNode @
- el = $.el 'img',
- id: 'ihover'
- src: post.file.URL
- el.setAttribute 'data-fullid', post.fullID
- $.add d.body, el
- UI.hover
- root: @
- el: el
- latestEvent: e
- endEvents: 'mouseout click'
- asapTest: -> el.naturalHeight
- $.on el, 'error', ImageHover.error
- error: ->
- return unless doc.contains @
- post = g.posts[@dataset.fullid]
-
- src = @src.split '/'
- if src[2] is 'images.4chan.org'
- if URL = Redirect.image src[3], src[5].replace /\?.+$/, ''
- @src = URL
- return
- if g.DEAD or post.isDead or post.file.isDead
- return
-
- timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
- # XXX CORS for images.4chan.org WHEN?
- $.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: ->
- return if @status isnt 200
- for postObj in JSON.parse(@response).posts
- break if postObj.no is post.ID
- if postObj.no isnt post.ID
- clearTimeout timeoutID
- post.kill()
- else if postObj.filedeleted
- clearTimeout timeoutID
- post.kill true
-
-ExpandComment =
- init: ->
- return if g.VIEW isnt 'index' or !Conf['Comment Expansion']
-
- Post::callbacks.push
- name: 'Comment Expansion'
- cb: @node
- node: ->
- if a = $ '.abbr > a', @nodes.comment
- $.on a, 'click', ExpandComment.cb
- cb: (e) ->
- e.preventDefault()
- post = Get.postFromNode @
- ExpandComment.expand post
- expand: (post) ->
- if post.nodes.longComment and !post.nodes.longComment.parentNode
- $.replace post.nodes.shortComment, post.nodes.longComment
- post.nodes.comment = post.nodes.longComment
- return
- return unless a = $ '.abbr > a', post.nodes.comment
- a.textContent = "Post No.#{post} Loading..."
- $.cache "//api.4chan.org#{a.pathname}.json", -> ExpandComment.parse @, a, post
- contract: (post) ->
- return unless post.nodes.shortComment
- a = $ '.abbr > a', post.nodes.shortComment
- a.textContent = 'here'
- $.replace post.nodes.longComment, post.nodes.shortComment
- post.nodes.comment = post.nodes.shortComment
- parse: (req, a, post) ->
- {status} = req
- if status not in [200, 304]
- a.textContent = "Error #{req.statusText} (#{status})"
- return
-
- posts = JSON.parse(req.response).posts
- if spoilerRange = posts[0].custom_spoiler
- Build.spoilerRange[g.BOARD] = spoilerRange
-
- for postObj in posts
- break if postObj.no is post.ID
- if postObj.no isnt post.ID
- a.textContent = "Post No.#{post} not found."
- return
-
- {comment} = post.nodes
- clone = comment.cloneNode false
- clone.innerHTML = postObj.com
- for quote in $$ '.quotelink', clone
- href = quote.getAttribute 'href'
- continue if href[0] is '/' # Cross-board quote, or board link
- quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
- post.nodes.shortComment = comment
- $.replace comment, clone
- post.nodes.comment = post.nodes.longComment = clone
- post.parseComment()
- post.parseQuotes()
- if Conf['Resurrect Quotes']
- Quotify.node.call post
- if Conf['Quote Previewing']
- QuotePreview.node.call post
- if Conf['Quote Inlining']
- QuoteInline.node.call post
- if Conf['Mark OP Quotes']
- QuoteOP.node.call post
- if Conf['Mark Cross-thread Quotes']
- QuoteCT.node.call post
- if g.BOARD.ID is 'g'
- Fourchan.code.call post
- if g.BOARD.ID is 'sci'
- Fourchan.math.call post
-
-ExpandThread =
- init: ->
- return if g.VIEW isnt 'index' or !Conf['Thread Expansion']
-
- Thread::callbacks.push
- name: 'Thread Expansion'
- cb: @node
- node: ->
- return unless span = $ '.summary', @OP.nodes.root.parentNode
- a = $.el 'a',
- textContent: "+ #{span.textContent}"
- className: 'summary'
- href: 'javascript:;'
- $.on a, 'click', ExpandThread.cbToggle
- $.replace span, a
-
- cbToggle: ->
- op = Get.postFromRoot @previousElementSibling
- ExpandThread.toggle op.thread
-
- toggle: (thread) ->
- threadRoot = thread.OP.nodes.root.parentNode
- a = $ '.summary', threadRoot
-
- switch thread.isExpanded
- when false, undefined
- thread.isExpanded = 'loading'
- for post in $$ '.thread > .postContainer', threadRoot
- ExpandComment.expand Get.postFromRoot post
- unless a
- thread.isExpanded = true
- return
- thread.isExpanded = 'loading'
- a.textContent = a.textContent.replace '+', '× Loading...'
- $.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", ->
- ExpandThread.parse @, thread, a
-
- when 'loading'
- thread.isExpanded = false
- return unless a
- a.textContent = a.textContent.replace '× Loading...', '+'
-
- when true
- thread.isExpanded = false
- if a
- a.textContent = a.textContent.replace '-', '+'
- #goddamit moot
- num = if thread.isSticky
- 1
- else switch g.BOARD.ID
- # XXX boards config
- when 'b', 'vg', 'q' then 3
- when 't' then 1
- else 5
- replies = $$('.thread > .replyContainer', threadRoot)[...-num]
- for reply in replies
- if Conf['Quote Inlining']
- # rm clones
- inlined.click() while inlined = $ '.inlined', reply
- $.rm reply
- for post in $$ '.thread > .postContainer', threadRoot
- ExpandComment.contract Get.postFromRoot post
- return
-
- parse: (req, thread, a) ->
- return if a.textContent[0] is '+'
- {status} = req
- if status not in [200, 304]
- a.textContent = "Error #{req.statusText} (#{status})"
- $.off a, 'click', ExpandThread.cb.toggle
- return
-
- thread.isExpanded = true
- a.textContent = a.textContent.replace '× Loading...', '-'
-
- posts = JSON.parse(req.response).posts
- if spoilerRange = posts[0].custom_spoiler
- Build.spoilerRange[g.BOARD] = spoilerRange
-
- replies = posts[1..]
- posts = []
- nodes = []
- for reply in replies
- if post = thread.posts[reply.no]
- nodes.push post.nodes.root
- continue
- node = Build.postFromObject reply, thread.board
- post = new Post node, thread, thread.board
- link = $ 'a[title="Highlight this post"]', node
- link.href = "res/#{thread}#p#{post}"
- link.nextSibling.href = "res/#{thread}#q#{post}"
- posts.push post
- nodes.push node
- Main.callbackNodes Post, posts
- $.after a, nodes
-
- # Enable 4chan features.
- if Conf['Enable 4chan\'s Extension']
- $.globalEval "Parser.parseThread(#{thread.ID}, 1, #{nodes.length})"
- else
- Fourchan.parseThread thread.ID, 1, nodes.length
-
-ThreadExcerpt =
- init: ->
- return if g.VIEW isnt 'thread' or !Conf['Thread Excerpt']
-
- Thread::callbacks.push
- name: 'Thread Excerpt'
- cb: @node
- node: ->
- d.title = Get.threadExcerpt @
-
-Unread =
- init: ->
- return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Tab Icon']
-
- @db = new DataBoard 'lastReadPosts', @sync
- @hr = $.el 'hr',
- id: 'unread-line'
- @posts = []
- @postsQuotingYou = []
-
- Thread::callbacks.push
- name: 'Unread'
- cb: @node
-
- node: ->
- Unread.thread = @
- Unread.title = d.title
- posts = []
- for ID, post of @posts
- posts.push post if post.isReply
- Unread.lastReadPost = Unread.db.get
- boardID: @board.ID
- threadID: @ID
- defaultValue: 0
- Unread.addPosts posts
- $.on d, 'ThreadUpdate', Unread.onUpdate
- $.on d, 'scroll visibilitychange', Unread.read
- $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line']
- $.on window, 'load', Unread.scroll if Conf['Scroll to Last Read Post']
-
- scroll: ->
- # Let the header's onload callback handle it.
- return if (hash = location.hash.match /\d+/) and hash[0] of @posts
- if Unread.posts.length
- # Scroll to before the first unread post.
- while root = $.x 'preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root
- break unless (Get.postFromRoot root).isHidden
- root.scrollIntoView false
- else if posts.length
- # Scroll to the last read post.
- Header.scrollToPost posts[posts.length - 1].nodes.root
-
- sync: ->
- lastReadPost = Unread.db.get
- boardID: Unread.thread.board.ID
- threadID: Unread.thread.ID
- defaultValue: 0
- return unless Unread.lastReadPost < lastReadPost
- Unread.lastReadPost = lastReadPost
- Unread.readArray Unread.posts
- Unread.readArray Unread.postsQuotingYou
- Unread.setLine()
- Unread.update()
-
- addPosts: (newPosts) ->
- for post in newPosts
- {ID} = post
- if ID <= Unread.lastReadPost or post.isHidden
- continue
- if QR.db
- data =
- boardID: post.board.ID
- threadID: post.thread.ID
- postID: post.ID
- continue if QR.db.get data
- Unread.posts.push post
- Unread.addPostQuotingYou post
- if Conf['Unread Line']
- # Force line on visible threads if there were no unread posts previously.
- Unread.setLine Unread.posts[0] in newPosts
- Unread.read()
- Unread.update()
-
- addPostQuotingYou: (post) ->
- return unless QR.db
- for quotelink in post.nodes.quotelinks
- if QR.db.get Get.postDataFromLink quotelink
- Unread.postsQuotingYou.push post
- return
-
- onUpdate: (e) ->
- if e.detail[404]
- Unread.update()
- else
- Unread.addPosts e.detail.newPosts
-
- readSinglePost: (post) ->
- return if (i = Unread.posts.indexOf post) is -1
- Unread.posts.splice i, 1
- if i is 0
- Unread.lastReadPost = post.ID
- Unread.saveLastReadPost()
- if (i = Unread.postsQuotingYou.indexOf post) isnt -1
- Unread.postsQuotingYou.splice i, 1
- Unread.update()
-
- readArray: (arr) ->
- for post, i in arr
- break if post.ID > Unread.lastReadPost
- arr.splice 0, i
-
- read: (e) ->
- return if d.hidden or !Unread.posts.length
- height = doc.clientHeight
- for post, i in Unread.posts
- {bottom} = post.nodes.root.getBoundingClientRect()
- break if bottom > height # post is not completely read
- return unless i
-
- Unread.lastReadPost = Unread.posts[i - 1].ID
- Unread.saveLastReadPost()
- Unread.posts.splice 0, i
- Unread.readArray Unread.postsQuotingYou
- Unread.update() if e
-
- saveLastReadPost: $.debounce 2 * $.SECOND, ->
- Unread.db.set
- boardID: Unread.thread.board.ID
- threadID: Unread.thread.ID
- val: Unread.lastReadPost
-
- setLine: (force) ->
- return unless d.hidden or force is true
- if post = Unread.posts[0]
- {root} = post.nodes
- if root isnt $ '.thread > .replyContainer', root.parentNode # not the first reply
- $.before root, Unread.hr
- else
- $.rm Unread.hr
-
- update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
- count = Unread.posts.length
-
- if Conf['Unread Count']
- d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}"
- <% if (type === 'crx') { %>
- # XXX Chrome bug where it doesn't always update the tab title.
- # crbug.com/124381
- # Call it one second later,
- # but don't display outdated unread count.
- unless dontrepeat
- setTimeout ->
- d.title = ''
- Unread.update true
- , $.SECOND
- <% } %>
-
- return unless Conf['Unread Tab Icon']
-
- Favicon.el.href =
- if g.DEAD
- if Unread.postsQuotingYou.length
- Favicon.unreadDeadY
- else if count
- Favicon.unreadDead
- else
- Favicon.dead
- else
- if count
- if Unread.postsQuotingYou.length
- Favicon.unreadY
- else
- Favicon.unread
- else
- Favicon.default
-
- <% if (type !== 'crx') { %>
- # `favicon.href = href` doesn't work on Firefox.
- # `favicon.href = href` isn't enough on Opera.
- # Opera won't always update the favicon if the href didn't change.
- $.add d.head, Favicon.el
- <% } %>
-
-Favicon =
- init: ->
- $.ready ->
- Favicon.el = $ 'link[rel="shortcut icon"]', d.head
- Favicon.el.type = 'image/x-icon'
- {href} = Favicon.el
- Favicon.SFW = /ws\.ico$/.test href
- Favicon.default = href
- Favicon.switch()
-
- switch: ->
- switch Conf['favicon']
- when 'ferongr'
- Favicon.unreadDead = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/ferongr/unreadDead.gif", {encoding: "base64"}) %>'
- Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>'
- Favicon.unreadSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/ferongr/unreadSFW.gif", {encoding: "base64"}) %>'
- Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>'
- Favicon.unreadNSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/ferongr/unreadNSFW.gif", {encoding: "base64"}) %>'
- Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>'
- when 'xat-'
- Favicon.unreadDead = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>'
- Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>'
- Favicon.unreadSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>'
- Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>'
- Favicon.unreadNSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>'
- Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>'
- when 'Mayhem'
- Favicon.unreadDead = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>'
- Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>'
- Favicon.unreadSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>'
- Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>'
- Favicon.unreadNSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>'
- Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>'
- when 'Original'
- Favicon.unreadDead = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/Original/unreadDead.gif", {encoding: "base64"}) %>'
- Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>'
- Favicon.unreadSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/Original/unreadSFW.gif", {encoding: "base64"}) %>'
- Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>'
- Favicon.unreadNSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/Original/unreadNSFW.gif", {encoding: "base64"}) %>'
- Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>'
- if Favicon.SFW
- Favicon.unread = Favicon.unreadSFW
- Favicon.unreadY = Favicon.unreadSFWY
- else
- Favicon.unread = Favicon.unreadNSFW
- Favicon.unreadY = Favicon.unreadNSFWY
-
- empty: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/empty.gif", {encoding: "base64"}) %>'
- dead: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/dead.gif", {encoding: "base64"}) %>'
-
-
-ThreadStats =
- init: ->
- return if g.VIEW isnt 'thread' or !Conf['Thread Stats']
- @dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', """
-
0 / 0
- """
-
- @postCountEl = $ '#post-count', @dialog
- @fileCountEl = $ '#file-count', @dialog
-
- Thread::callbacks.push
- name: 'Thread Stats'
- cb: @node
- node: ->
- postCount = 0
- fileCount = 0
- for ID, post of @posts
- postCount++
- fileCount++ if post.file
- ThreadStats.thread = @
- ThreadStats.update postCount, fileCount
- $.on d, 'ThreadUpdate', ThreadStats.onUpdate
- $.add d.body, ThreadStats.dialog
- onUpdate: (e) ->
- return if e.detail[404]
- {postCount, fileCount} = e.detail
- ThreadStats.update postCount, fileCount
- update: (postCount, fileCount) ->
- {thread, postCountEl, fileCountEl} = ThreadStats
- postCountEl.textContent = postCount
- fileCountEl.textContent = fileCount
- (if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning'
- (if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning'
-
-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 += ""
-
- checked = if Conf['Auto Update'] then 'checked' else ''
- html = """
-
- #{html}
-
-
-
- """
-
- @dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html
- @timer = $ '#update-timer', @dialog
- @status = $ '#update-status', @dialog
-
- 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'
- $.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
-
- ###
- http://freesound.org/people/pierrecartoons1979/sounds/90112/
- cc-by-nc-3.0
- ###
- 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 Conf['Auto Update This']
- ThreadUpdater.set 'status', null, null
- else
- ThreadUpdater.set 'timer', null
- ThreadUpdater.set 'status', 'Offline', 'warning'
- ThreadUpdater.cb.autoUpdate()
- post: (e) ->
- return unless Conf['Auto Update This'] 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: ->
- if Conf['Auto Update This'] and ThreadUpdater.online
- ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000
- else
- clearTimeout ThreadUpdater.timeoutID
- interval: ->
- val = Math.max 5, parseInt @value, 10
- ThreadUpdater.interval = @value = val
- $.cb.value.call @
- 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
- 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)
-
-ThreadWatcher =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Thread Watcher']
- @dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;',
- '