QR =
init: ->
return if !Conf['Quick Reply']
@db = new DataBoard 'yourPosts'
if Conf['QR Shortcut']
sc = $.el 'a',
className: "qr-shortcut #{unless Conf['Persistent QR'] then 'disabled' else ''}"
textContent: 'QR'
title: 'Quick Reply'
href: 'javascript:;'
$.on sc, 'click', ->
if Conf['Persistent QR'] or !QR.nodes or QR.nodes.el.hidden
$.event 'CloseMenu'
QR.open()
QR.nodes.com.focus()
$.rmClass @, 'disabled'
else
QR.close()
$.addClass @, 'disabled'
Header.addShortcut sc
if Conf['Hide Original Post Form']
$.asap (-> doc), -> $.addClass doc, 'hide-original-post-form'
$.ready @initReady
if Conf['Persistent QR']
unless g.BOARD.ID is 'f' and g.VIEW is 'index'
$.on d, '4chanXInitFinished', @persist
else
$.ready @persist
Post::callbacks.push
name: 'Quick Reply'
cb: @node
initReady: ->
QR.postingIsEnabled = !!$.id 'postForm'
return unless QR.postingIsEnabled
link = $.el 'h1',
innerHTML: "#{if g.VIEW is 'thread' then 'Reply to Thread' else 'Start a Thread'}"
className: "qr-link-container"
$.on link.firstChild, 'click', ->
$.event 'CloseMenu'
QR.open()
QR.nodes.com.focus()
if Conf['QR Shortcut']
$.rmClass $('.qr-shortcut'), 'disabled'
$.before $.id('postForm'), link
$.on d, 'QRGetSelectedPost', ({detail: cb}) ->
cb QR.selected
$.on d, 'QRAddPreSubmitHook', ({detail: cb}) ->
QR.preSubmitHooks.push cb
<% if (type === 'crx') { %>
$.on d, 'paste', QR.paste
<% } %>
$.on d, 'dragover', QR.dragOver
$.on d, 'drop', QR.dropFile
$.on d, 'dragstart dragend', QR.drag
$.on d, 'ThreadUpdate', ->
if g.DEAD
QR.abort()
else
QR.status()
node: ->
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
persist: ->
return unless QR.postingIsEnabled
QR.open()
QR.hide() if Conf['Auto Hide QR']
open: ->
if QR.nodes
QR.nodes.el.hidden = false
QR.unhide()
return
try
QR.dialog()
catch err
delete QR.nodes
Main.handleErrors
message: 'Quick Reply dialog creation crashed.'
error: err
close: ->
if QR.req
QR.abort()
return
QR.nodes.el.hidden = true
QR.cleanNotifications()
d.activeElement.blur()
$.rmClass QR.nodes.el, 'dump'
unless Conf['Captcha Warning Notifications']
$.rmClass QR.captcha.nodes.input, 'error'
if Conf['QR Shortcut']
$.toggleClass $('.qr-shortcut'), 'disabled'
for i in QR.posts
QR.posts[0].rm()
QR.cooldown.auto = false
QR.status()
focusin: ->
$.addClass QR.nodes.el, 'has-focus'
focusout: ->
$.rmClass QR.nodes.el, 'has-focus'
hide: ->
d.activeElement.blur()
$.addClass QR.nodes.el, 'autohide'
QR.nodes.autohide.checked = true
unhide: ->
$.rmClass QR.nodes.el, 'autohide'
QR.nodes.autohide.checked = false
toggleHide: ->
if @checked
QR.hide()
else
QR.unhide()
error: (err) ->
QR.open()
if typeof err is 'string'
el = $.tn err
else
el = err
el.removeAttribute 'style'
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
# Focus the captcha input on captcha error.
QR.captcha.nodes.input.focus()
if Conf['Captcha Warning Notifications']
QR.notifications.push new Notification 'warning', el
else
$.addClass QR.captcha.nodes.input, 'error'
$.on QR.captcha.nodes.input, 'keydown', ->
$.rmClass QR.captcha.nodes.input, 'error'
else
QR.notifications.push new Notification 'warning', el
alert el.textContent if d.hidden
notifications: []
cleanNotifications: ->
for notification in QR.notifications
notification.close()
QR.notifications = []
status: ->
return unless QR.nodes
if g.DEAD
value = 404
disabled = true
QR.cooldown.auto = false
value = if QR.req
QR.req.progress
else
QR.cooldown.seconds or value
{status} = QR.nodes
status.value = unless value
'Submit'
else if QR.cooldown.auto
"Auto #{value}"
else
value
status.disabled = disabled or false
persona:
pwd: ''
always: {}
init: ->
QR.persona.getPassword()
$.get 'QR.personas', Conf['QR.personas'], ({'QR.personas': personas}) ->
types =
name: []
email: []
sub: []
for item in personas.split '\n'
QR.persona.parseItem item.trim(), types
for type, arr of types
QR.persona.loadPersonas type, arr
return
parseItem: (item, types) ->
return if item[0] is '#'
return unless match = item.match /(name|email|subject|password):"(.*)"/i
[match, type, val] = match
# Don't mix up item settings with val.
item = item.replace match, ''
boards = item.match(/boards:([^;]+)/i)?[1].toLowerCase() or 'global'
if boards isnt 'global' and not ((boards.split ',').contains g.BOARD.ID)
return
if type is 'password'
QR.persona.pwd = val
return
type = 'sub' if type is 'subject'
if /always/i.test item
QR.persona.always[type] = val
unless types[type].contains val
types[type].push val
loadPersonas: (type, arr) ->
list = $ "#list-#{type}", QR.nodes.el
for val in arr
# XXX Firefox displays empty
"""
$.add nodes.form, nodes.flashTag
# Make a list of threads.
for thread of g.BOARD.threads
$.add nodes.thread, $.el 'option',
value: thread
textContent: "Thread No.#{thread}"
$.on nodes.filename.parentNode, 'click keyup', QR.openFileInput
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
for elm in $$ '*', QR.nodes.el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
$.on dialog, 'focusin', QR.focusin
$.on dialog, 'focusout', QR.focusout
$.on nodes.autohide, 'change', QR.toggleHide
$.on nodes.close, 'click', QR.close
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
$.on nodes.addPost, 'click', -> new QR.post true
$.on nodes.form, 'submit', QR.submit
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
$.on nodes.fileInput, 'change', QR.fileInput
# save selected post's data
for name in ['name', 'email', 'sub', 'com']
$.on nodes[name], 'input', -> QR.selected.save @
$.on nodes.thread, 'change', -> QR.selected.save @
<% if (type === 'userscript') { %>
if Conf['Remember QR Size']
$.get 'QR Size', '', (item) ->
nodes.com.style.cssText = item['QR Size']
$.on nodes.com, 'mouseup', (e) ->
return if e.button isnt 0
$.set 'QR Size', @style.cssText
<% } %>
QR.persona.init()
new QR.post true
QR.status()
QR.cooldown.init()
QR.captcha.init()
$.add d.body, dialog
# Create a custom event when the QR dialog is first initialized.
# Use it to extend the QR's functionalities, or for XTRM RICE.
$.event 'QRDialogCreation', null, dialog
preSubmitHooks: []
submit: (e) ->
e?.preventDefault()
if QR.req
QR.abort()
return
if QR.cooldown.seconds
QR.cooldown.auto = !QR.cooldown.auto
QR.status()
return
post = QR.posts[0]
post.forceSave()
if g.BOARD.ID is 'f'
filetag = QR.nodes.flashTag.value
threadID = post.thread
thread = g.BOARD.threads[threadID]
# prevent errors
if threadID is 'new'
threadID = null
if ['vg', 'q'].contains(g.BOARD.ID) and !post.sub
err = 'New threads require a subject.'
else unless post.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
err = 'No file selected.'
else if g.BOARD.threads[threadID].isClosed
err = 'You can\'t reply to this thread anymore.'
else unless post.com or post.file
err = 'No file selected.'
else if post.file and thread.fileLimit
err = 'Max limit of image replies has been reached.'
else for hook in QR.preSubmitHooks
if err = hook post, thread
break
if QR.captcha.isEnabled and !err
{challenge, response} = QR.captcha.getOne()
err = 'No valid captcha.' unless response
QR.cleanNotifications()
if err
# stop auto-posting
QR.cooldown.auto = false
QR.status()
QR.error err
return
# Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.posts.length > 1
if Conf['Auto Hide QR'] and !QR.cooldown.auto
QR.hide()
if !QR.cooldown.auto and $.x 'ancestor::div[@id="qr"]', d.activeElement
# Unfocus the focused element if it is one within the QR and we're not auto-posting.
d.activeElement.blur()
post.lock()
postData =
resto: threadID
name: post.name
email: post.email
sub: post.sub
com: post.com
upfile: post.file
filetag: filetag
spoiler: post.spoiler
textonly: textOnly
mode: 'regist'
pwd: QR.persona.pwd
recaptcha_challenge_field: challenge
recaptcha_response_field: response
callbacks =
onload: QR.response
onerror: ->
# Connection error, or
# www.4chan.org/banned
delete QR.req
post.unlock()
QR.cooldown.auto = false
QR.status()
QR.error $.el 'span',
innerHTML: """
4chan X encountered an error while posting.
[Banned?] [More info]
"""
opts =
cred: true
form: $.formData postData
upCallbacks:
onload: ->
# Upload done, waiting for server response.
QR.req.isUploadFinished = true
QR.req.uploadEndTime = Date.now()
QR.req.progress = '...'
QR.status()
onprogress: (e) ->
# Uploading...
QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
QR.status()
QR.req = $.ajax $.id('postForm').parentNode.action, callbacks, opts
# Starting to upload might take some time.
# Provide some feedback that we're starting to submit.
QR.req.uploadStartTime = Date.now()
QR.req.progress = '...'
QR.status()
response: ->
<% if (type === 'userjs') { %>
# The upload.onload callback is not called
# or at least not in time with Opera.
QR.req.upload.onload()
<% } %>
{req} = QR
delete QR.req
post = QR.posts[0]
post.unlock()
tmpDoc = d.implementation.createHTMLDocument ''
tmpDoc.documentElement.innerHTML = req.response
if ban = $ '.banType', tmpDoc # banned/warning
board = $('.board', tmpDoc).innerHTML
err = $.el 'span', innerHTML:
if ban.textContent.toLowerCase() is 'banned'
"You are banned on #{board}! ;_;
" +
"Click here to see the reason."
else
"You were issued a warning on #{board} as #{$('.nameBlock', tmpDoc).innerHTML}.
" +
"Reason: #{$('.reason', tmpDoc).innerHTML}"
else if err = tmpDoc.getElementById 'errmsg' # error!
$('a', err)?.target = '_blank' # duplicate image link
else if tmpDoc.title isnt 'Post successful!'
err = 'Connection error with sys.4chan.org.'
else if req.status isnt 200
err = "Error #{req.statusText} (#{req.status})"
if err
if /captcha|verification/i.test(err.textContent) or err is 'Connection error with sys.4chan.org.'
# Remove the obnoxious 4chan Pass ad.
if /mistyped/i.test err.textContent
err = 'You seem to have mistyped the CAPTCHA.'
# Enable auto-post if we have some cached captchas.
QR.cooldown.auto = if QR.captcha.isEnabled
!!QR.captcha.captchas.length
else if err is 'Connection error with sys.4chan.org.'
true
else
# Something must've gone terribly wrong if you get captcha errors without captchas.
# Don't auto-post indefinitely in that case.
false
# Too many frequent mistyped captchas will auto-ban you!
# On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2
else if err.textContent and m = err.textContent.match /wait\s(\d+)\ssecond/i
QR.cooldown.auto = if QR.captcha.isEnabled
!!QR.captcha.captchas.length
else
true
QR.cooldown.set delay: m[1]
else # stop auto-posting
QR.cooldown.auto = false
QR.status()
QR.error err
return
QR.cleanNotifications()
h1 = $ 'h1', tmpDoc
if Conf['Posting Success Notifications']
QR.notifications.push new Notification 'success', h1.textContent, 5
QR.persona.set post
[_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
postID = +postID
threadID = +threadID or postID
isReply = threadID isnt postID
QR.db.set
boardID: g.BOARD.ID
threadID: threadID
postID: postID
val: true
ThreadUpdater.postID = postID
# Post/upload confirmed as successful.
$.event 'QRPostSuccessful', {
board: g.BOARD
threadID
postID
}
# Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.posts.length > 1 and isReply
unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close()
else
post.rm()
QR.cooldown.set {req, post, isReply}
if threadID is postID # new thread
URL = "/#{g.BOARD}/res/#{threadID}"
else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index
URL = "/#{g.BOARD}/res/#{threadID}#p#{postID}"
if URL
if Conf['Open Post in New Tab']
$.open "/#{g.BOARD}/res/#{threadID}"
else
window.location = "/#{g.BOARD}/res/#{threadID}"
QR.status()
abort: ->
if QR.req and !QR.req.isUploadFinished
QR.req.abort()
delete QR.req
QR.posts[0].unlock()
QR.notifications.push new Notification 'info', 'QR upload aborted.', 5
QR.status()