836 lines
27 KiB
CoffeeScript
836 lines
27 KiB
CoffeeScript
QR =
|
|
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm']
|
|
|
|
validExtension: /\.(jpe?g|png|gif|pdf|swf|webm)$/i
|
|
|
|
typeFromExtension:
|
|
'jpg': 'image/jpeg'
|
|
'jpeg': 'image/jpeg'
|
|
'png': 'image/png'
|
|
'gif': 'image/gif'
|
|
'pdf': 'application/pdf'
|
|
'swf': 'application/vnd.adobe.flash.movie'
|
|
'webm': 'video/webm'
|
|
|
|
init: ->
|
|
return unless Conf['Quick Reply']
|
|
|
|
@db = new DataBoard 'yourPosts'
|
|
@posts = []
|
|
|
|
return if g.VIEW is 'archive'
|
|
|
|
version = if Conf['Use Recaptcha v1'] then 'v1' else 'v2'
|
|
@captcha = Captcha[version]
|
|
|
|
$.on d, '4chanXInitFinished', @initReady
|
|
|
|
Post.callbacks.push
|
|
name: 'Quick Reply'
|
|
cb: @node
|
|
|
|
if Conf['QR Shortcut']
|
|
@shortcut = sc = $.el 'a',
|
|
className: 'qr-shortcut fa fa-comment-o disabled'
|
|
textContent: 'QR'
|
|
title: 'Quick Reply'
|
|
href: 'javascript:;'
|
|
$.on sc, 'click', ->
|
|
return unless QR.postingIsEnabled
|
|
if Conf['Persistent QR'] or !QR.nodes or QR.nodes.el.hidden
|
|
QR.open()
|
|
QR.nodes.com.focus()
|
|
else
|
|
QR.close()
|
|
|
|
Header.addShortcut sc
|
|
|
|
if Conf['Hide Original Post Form']
|
|
$.addClass doc, 'hide-original-post-form'
|
|
unless $.hasClass doc, 'js-enabled'
|
|
# Prevent unnecessary loading of fallback iframe.
|
|
$.onExists doc, '#postForm noscript', true, $.rm
|
|
|
|
initReady: ->
|
|
$.off d, '4chanXInitFinished', @initReady
|
|
QR.postingIsEnabled = !!$.id 'postForm'
|
|
return unless QR.postingIsEnabled
|
|
|
|
link = $.el 'h1',
|
|
className: "qr-link-container"
|
|
$.extend link, <%= html('<a href="javascript:;" class="qr-link">?{g.VIEW === "thread"}{Reply to Thread}{Start a Thread}</a>') %>
|
|
|
|
QR.link = link.firstElementChild
|
|
$.on link.firstChild, 'click', ->
|
|
QR.open()
|
|
QR.nodes.com.focus()
|
|
|
|
if Conf['Bottom QR Link'] and g.VIEW is 'thread'
|
|
linkBot = $.el 'div',
|
|
className: "brackets-wrap qr-link-container-bottom"
|
|
$.extend linkBot, <%= html('<a href="javascript:;" class="qr-link-bottom">Reply to Thread</a>') %>
|
|
|
|
$.on linkBot.firstElementChild, 'click', ->
|
|
QR.open()
|
|
QR.nodes.com.focus()
|
|
|
|
$.prepend $('.navLinksBot'), linkBot
|
|
|
|
$.before $.id('togglePostFormLink'), link
|
|
|
|
$.on d, 'QRGetFile', QR.getFile
|
|
$.on d, 'QRSetFile', QR.setFile
|
|
|
|
$.on d, 'paste', QR.paste
|
|
$.on d, 'dragover', QR.dragOver
|
|
$.on d, 'drop', QR.dropFile
|
|
$.on d, 'dragstart dragend', QR.drag
|
|
|
|
$.on d, 'IndexRefresh', QR.generatePostableThreadsList
|
|
$.on d, 'ThreadUpdate', QR.statusCheck
|
|
|
|
return if !Conf['Persistent QR']
|
|
QR.open()
|
|
QR.hide() if Conf['Auto Hide QR']
|
|
|
|
statusCheck: ->
|
|
return unless QR.nodes
|
|
{thread} = QR.posts[0]
|
|
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead
|
|
QR.abort()
|
|
else
|
|
QR.status()
|
|
|
|
node: ->
|
|
$.on @nodes.quote, 'click', QR.quote
|
|
QR.generatePostableThreadsList() if @isFetchedQuote
|
|
|
|
open: ->
|
|
if QR.nodes
|
|
QR.captcha.setup() if QR.nodes.el.hidden
|
|
QR.nodes.el.hidden = false
|
|
QR.unhide()
|
|
else
|
|
try
|
|
QR.dialog()
|
|
catch err
|
|
delete QR.nodes
|
|
Main.handleErrors
|
|
message: 'Quick Reply dialog creation crashed.'
|
|
error: err
|
|
return
|
|
if Conf['QR Shortcut']
|
|
$.rmClass QR.shortcut, 'disabled'
|
|
|
|
close: ->
|
|
if QR.req
|
|
QR.abort()
|
|
return
|
|
QR.nodes.el.hidden = true
|
|
QR.cleanNotifications()
|
|
d.activeElement.blur()
|
|
$.rmClass QR.nodes.el, 'dump'
|
|
if Conf['QR Shortcut']
|
|
$.addClass QR.shortcut, 'disabled'
|
|
new QR.post true
|
|
for post in QR.posts.splice 0, QR.posts.length - 1
|
|
post.delete()
|
|
QR.cooldown.auto = false
|
|
QR.status()
|
|
QR.captcha.destroy()
|
|
|
|
focus: ->
|
|
$.queueTask ->
|
|
unless QR.inBubble()
|
|
QR.hasFocus = d.activeElement and QR.nodes.el.contains(d.activeElement)
|
|
QR.nodes.el.classList.toggle 'focus', QR.hasFocus
|
|
# XXX Stop unwanted scrolling due to captcha.
|
|
if QR.captcha.isEnabled and QR.captcha is Captcha.v2 and !QR.captcha.noscript
|
|
if QR.inCaptcha()
|
|
QR.scrollY = window.scrollY
|
|
$.on d, 'scroll', QR.scrollLock
|
|
else
|
|
$.off d, 'scroll', QR.scrollLock
|
|
|
|
inBubble: ->
|
|
bubbles = $$ '.goog-bubble-content > iframe'
|
|
d.activeElement in bubbles or bubbles.some((el) -> el.getBoundingClientRect().bottom > 0)
|
|
|
|
inCaptcha: ->
|
|
(d.activeElement?.nodeName is 'IFRAME' and QR.nodes.el.contains(d.activeElement)) or (QR.hasFocus and QR.inBubble())
|
|
|
|
scrollLock: ->
|
|
if QR.inCaptcha()
|
|
window.scroll window.scrollX, QR.scrollY
|
|
else
|
|
$.off d, 'scroll', QR.scrollLock
|
|
|
|
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()
|
|
|
|
setCustomCooldown: (enabled) ->
|
|
Conf['customCooldownEnabled'] = enabled
|
|
QR.cooldown.customCooldown = enabled
|
|
QR.nodes.customCooldown.classList.toggle 'disabled', !enabled
|
|
|
|
toggleCustomCooldown: ->
|
|
enabled = $.hasClass @, 'disabled'
|
|
QR.setCustomCooldown enabled
|
|
$.set 'customCooldownEnabled', enabled
|
|
|
|
error: (err, focusOverride) ->
|
|
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
|
|
QR.captcha.setup true
|
|
notice = new Notice 'warning', el
|
|
QR.notifications.push notice
|
|
unless Header.areNotificationsEnabled
|
|
alert el.textContent if d.hidden and not QR.cooldown.auto
|
|
else if d.hidden or not (focusOverride or d.hasFocus())
|
|
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1130502 (SeaMonkey)
|
|
try
|
|
notif = new Notification el.textContent,
|
|
body: el.textContent
|
|
icon: Favicon.logo
|
|
notif.onclick = -> window.focus()
|
|
if $.engine isnt 'gecko'
|
|
# Firefox automatically closes notifications
|
|
# so we can't control the onclose properly.
|
|
notif.onclose = -> notice.close()
|
|
notif.onshow = ->
|
|
setTimeout ->
|
|
notif.onclose = null
|
|
notif.close()
|
|
, 7 * $.SECOND
|
|
|
|
notifications: []
|
|
|
|
cleanNotifications: ->
|
|
for notification in QR.notifications
|
|
notification.close()
|
|
QR.notifications = []
|
|
|
|
status: ->
|
|
return unless QR.nodes
|
|
{thread} = QR.posts[0]
|
|
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead
|
|
value = 'Dead'
|
|
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
|
|
|
|
quote: (e) ->
|
|
e?.preventDefault()
|
|
return unless QR.postingIsEnabled
|
|
|
|
sel = d.getSelection()
|
|
post = Get.postFromNode @
|
|
text = if post.board.ID is g.BOARD.ID then ">>#{post}\n" else ">>>/#{post.board}/#{post}\n"
|
|
if sel.toString().trim() and post is Get.postFromNode sel.anchorNode
|
|
range = sel.getRangeAt 0
|
|
frag = range.cloneContents()
|
|
ancestor = range.commonAncestorContainer
|
|
# Quoting the insides of a spoiler/code tag.
|
|
if $.x 'ancestor-or-self::*[self::s or contains(@class,"removed-spoiler")]', ancestor
|
|
$.prepend frag, $.tn '[spoiler]'
|
|
$.add frag, $.tn '[/spoiler]'
|
|
if insideCode = $.x 'ancestor-or-self::pre[contains(@class,"prettyprint")]', ancestor
|
|
$.prepend frag, $.tn '[code]'
|
|
$.add frag, $.tn '[/code]'
|
|
for node in $$ (if insideCode then 'br' else '.prettyprint br'), frag
|
|
$.replace node, $.tn '\n'
|
|
for node in $$ 'br', frag
|
|
$.replace node, $.tn '\n>' unless node is frag.lastChild
|
|
for node in $$ 's, .removed-spoiler', frag
|
|
$.replace node, [$.tn('[spoiler]'), node.childNodes..., $.tn '[/spoiler]']
|
|
for node in $$ '.prettyprint', frag
|
|
$.replace node, [$.tn('[code]'), node.childNodes..., $.tn '[/code]']
|
|
for node in $$ '.linkify[data-original]', frag
|
|
$.replace node, $.tn node.dataset.original
|
|
for node in $$ '.embedder', frag
|
|
$.rm node.previousSibling if node.previousSibling?.nodeValue is ' '
|
|
$.rm node
|
|
text += ">#{frag.textContent.trim()}\n"
|
|
|
|
QR.open()
|
|
if QR.selected.isLocked
|
|
index = QR.posts.indexOf QR.selected
|
|
(QR.posts[index+1] or new QR.post()).select()
|
|
$.addClass QR.nodes.el, 'dump'
|
|
QR.cooldown.auto = true
|
|
{com, thread} = QR.nodes
|
|
thread.value = Get.threadFromNode @ unless com.value
|
|
|
|
caretPos = com.selectionStart
|
|
# Replace selection for text.
|
|
com.value = com.value[...caretPos] + text + com.value[com.selectionEnd..]
|
|
# Move the caret to the end of the new quote.
|
|
range = caretPos + text.length
|
|
com.setSelectionRange range, range
|
|
com.focus()
|
|
|
|
QR.selected.save com
|
|
QR.selected.save thread
|
|
|
|
characterCount: ->
|
|
counter = QR.nodes.charCount
|
|
count = QR.nodes.com.value.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '_').length
|
|
counter.textContent = count
|
|
counter.hidden = count < QR.max_comment/2
|
|
(if count > QR.max_comment then $.addClass else $.rmClass) counter, 'warning'
|
|
|
|
getFile: ->
|
|
$.event 'QRFile', QR.selected?.file
|
|
|
|
setFile: (e) ->
|
|
{file, name} = e.detail
|
|
file.name = name if name?
|
|
QR.open()
|
|
QR.handleFiles [file]
|
|
|
|
drag: (e) ->
|
|
# Let it drag anything from the page.
|
|
toggle = if e.type is 'dragstart' then $.off else $.on
|
|
toggle d, 'dragover', QR.dragOver
|
|
toggle d, 'drop', QR.dropFile
|
|
|
|
dragOver: (e) ->
|
|
e.preventDefault()
|
|
e.dataTransfer.dropEffect = 'copy' # cursor feedback
|
|
|
|
dropFile: (e) ->
|
|
# Let it only handle files from the desktop.
|
|
return unless e.dataTransfer.files.length
|
|
e.preventDefault()
|
|
QR.open()
|
|
QR.handleFiles e.dataTransfer.files
|
|
|
|
paste: (e) ->
|
|
return unless e.clipboardData.items
|
|
files = []
|
|
for item in e.clipboardData.items when item.kind is 'file'
|
|
blob = item.getAsFile()
|
|
blob.name = 'file'
|
|
blob.name += '.' + blob.type.split('/')[1] if blob.type
|
|
files.push blob
|
|
return unless files.length
|
|
QR.open()
|
|
QR.handleFiles files
|
|
$.addClass QR.nodes.el, 'dump'
|
|
|
|
pasteFF: ->
|
|
{pasteArea} = QR.nodes
|
|
return unless pasteArea.childNodes.length
|
|
images = $$ 'img', pasteArea
|
|
$.rmAll pasteArea
|
|
for img in images
|
|
{src} = img
|
|
if m = src.match /data:(image\/(\w+));base64,(.+)/
|
|
bstr = atob m[3]
|
|
arr = new Uint8Array(bstr.length)
|
|
for i in [0...bstr.length]
|
|
arr[i] = bstr.charCodeAt(i)
|
|
blob = new Blob [arr], {type: m[1]}
|
|
blob.name = "file.#{m[2]}"
|
|
QR.handleFiles [blob]
|
|
else if /^https?:\/\//.test src
|
|
QR.handleUrl src
|
|
return
|
|
|
|
handleUrl: (urlDefault) ->
|
|
url = prompt 'Enter a URL:', urlDefault
|
|
return if url is null
|
|
QR.nodes.fileButton.focus()
|
|
CrossOrigin.file url, (blob) ->
|
|
if blob
|
|
QR.handleFiles [blob]
|
|
else
|
|
QR.error "Can't load image."
|
|
|
|
handleFiles: (files) ->
|
|
if @ isnt QR # file input
|
|
files = [@files...]
|
|
@value = null
|
|
return unless files.length
|
|
QR.cleanNotifications()
|
|
for file in files
|
|
QR.handleFile file, files.length
|
|
$.addClass QR.nodes.el, 'dump' unless files.length is 1
|
|
if d.activeElement is QR.nodes.fileButton and $.hasClass QR.nodes.fileSubmit, 'has-file'
|
|
QR.nodes.filename.focus()
|
|
|
|
handleFile: (file, nfiles) ->
|
|
isText = /^text\//.test file.type
|
|
if nfiles is 1
|
|
post = QR.selected
|
|
else
|
|
post = QR.posts[QR.posts.length - 1]
|
|
if (if isText then post.com or post.pasting else post.file)
|
|
post = new QR.post()
|
|
post[if isText then 'pasteText' else 'setFile'] file
|
|
|
|
openFileInput: ->
|
|
return if QR.nodes.fileButton.disabled
|
|
QR.nodes.fileInput.click()
|
|
QR.nodes.fileButton.focus()
|
|
|
|
generatePostableThreadsList: ->
|
|
return unless QR.nodes
|
|
list = QR.nodes.thread
|
|
options = [list.firstElementChild]
|
|
for thread in g.BOARD.threads.keys
|
|
options.push $.el 'option',
|
|
value: thread
|
|
textContent: "Thread #{thread}"
|
|
val = list.value
|
|
$.rmAll list
|
|
$.add list, options
|
|
list.value = val
|
|
return if list.value is val
|
|
# Fix the value if the option disappeared.
|
|
list.value = if g.VIEW is 'thread'
|
|
g.THREADID
|
|
else
|
|
'new'
|
|
(if g.VIEW is 'thread' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
|
|
|
|
dialog: ->
|
|
QR.nodes = nodes =
|
|
el: dialog = UI.dialog 'qr', 'top: 50px; right: 0px;',
|
|
<%= importHTML('Features/QuickReply') %>
|
|
|
|
setNode = (name, query) ->
|
|
nodes[name] = $ query, dialog
|
|
|
|
setNode 'move', '.move'
|
|
setNode 'autohide', '#autohide'
|
|
setNode 'thread', 'select'
|
|
setNode 'threadPar', '#qr-thread-select'
|
|
setNode 'close', '.close'
|
|
setNode 'form', 'form'
|
|
setNode 'dumpButton', '#dump-button'
|
|
setNode 'pasteArea', '#paste-area'
|
|
setNode 'urlButton', '#url-button'
|
|
setNode 'name', '[data-name=name]'
|
|
setNode 'email', '[data-name=email]'
|
|
setNode 'sub', '[data-name=sub]'
|
|
setNode 'com', '[data-name=com]'
|
|
setNode 'dumpList', '#dump-list'
|
|
setNode 'addPost', '#add-post'
|
|
setNode 'charCount', '#char-count'
|
|
setNode 'fileSubmit', '#file-n-submit'
|
|
setNode 'fileButton', '#qr-file-button'
|
|
setNode 'noFile', '#qr-no-file'
|
|
setNode 'filename', '#qr-filename'
|
|
setNode 'fileRM', '#qr-filerm'
|
|
setNode 'spoiler', '#qr-file-spoiler'
|
|
setNode 'spoilerPar', '#qr-spoiler-label'
|
|
setNode 'status', '[type=submit]'
|
|
setNode 'fileInput', '[type=file]'
|
|
setNode 'customCooldown', '#custom-cooldown-button'
|
|
|
|
rules = $('ul.rules').textContent.trim()
|
|
match_min = rules.match(/.+smaller than (\d+)x(\d+).+/)
|
|
match_max = rules.match(/.+greater than (\d+)x(\d+).+/)
|
|
QR.min_width = +match_min?[1] or 1
|
|
QR.min_height = +match_min?[2] or 1
|
|
QR.max_width = +match_max?[1] or 10000
|
|
QR.max_height = +match_max?[2] or 10000
|
|
|
|
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
|
|
|
|
scriptData = Get.scriptData()
|
|
QR.max_size_video = if (m = scriptData.match /\bmaxWebmFilesize *= *(\d+)\b/) then +m[1] else +nodes.fileInput.max
|
|
QR.max_comment = if (m = scriptData.match /\bcomlen *= *(\d+)\b/) then +m[1] else 2000
|
|
|
|
QR.max_width_video = QR.max_height_video = 2048
|
|
QR.max_duration_video = if g.BOARD.ID in ['gif', 'wsg'] then 300 else 120
|
|
|
|
if Conf['Show New Thread Option in Threads']
|
|
$.addClass QR.nodes.el, 'show-new-thread-option'
|
|
|
|
if Conf['Show Name and Subject']
|
|
$.addClass QR.nodes.name, 'force-show'
|
|
$.addClass QR.nodes.sub, 'force-show'
|
|
QR.nodes.email.placeholder = 'E-mail'
|
|
|
|
QR.forcedAnon = !!$ 'form[name="post"] input[name="name"][type="hidden"]'
|
|
if QR.forcedAnon
|
|
$.addClass QR.nodes.el, 'forced-anon'
|
|
|
|
QR.spoiler = !!$ '.postForm input[name=spoiler]'
|
|
if QR.spoiler
|
|
$.addClass QR.nodes.el, 'has-spoiler'
|
|
else
|
|
nodes.spoiler.parentElement.hidden = true
|
|
|
|
if parseInt(Conf['customCooldown'], 10) > 0
|
|
$.addClass QR.nodes.fileSubmit, 'custom-cooldown'
|
|
$.get 'customCooldownEnabled', Conf['customCooldownEnabled'], ({customCooldownEnabled}) ->
|
|
QR.setCustomCooldown customCooldownEnabled
|
|
$.sync 'customCooldownEnabled', QR.setCustomCooldown
|
|
|
|
if g.BOARD.ID is 'f'
|
|
nodes.flashTag = $.el 'select',
|
|
name: 'filetag'
|
|
|
|
$.extend nodes.flashTag, <%= html(
|
|
'<option value="0">Hentai</option>' +
|
|
'<option value="6">Porn</option>' +
|
|
'<option value="1">Japanese</option>' +
|
|
'<option value="2">Anime</option>' +
|
|
'<option value="3">Game</option>' +
|
|
'<option value="5">Loop</option>' +
|
|
'<option value="4" selected>Other</option>'
|
|
) %>
|
|
|
|
nodes.flashTag.dataset.default = '4'
|
|
$.add nodes.form, nodes.flashTag
|
|
|
|
$.on nodes.fileButton, 'click', QR.openFileInput
|
|
$.on nodes.noFile, 'click', QR.openFileInput
|
|
$.on nodes.filename, 'focus', -> $.addClass @parentNode, 'focus'
|
|
$.on nodes.filename, 'blur', -> $.rmClass @parentNode, 'focus'
|
|
$.on nodes.autohide, 'change', QR.toggleHide
|
|
$.on nodes.close, 'click', QR.close
|
|
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
|
|
$.on nodes.urlButton, 'click', -> QR.handleUrl ''
|
|
$.on nodes.addPost, 'click', -> new QR.post true
|
|
$.on nodes.form, 'submit', QR.submit
|
|
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
|
|
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
|
|
$.on nodes.fileInput, 'change', QR.handleFiles
|
|
$.on nodes.customCooldown, 'click', QR.toggleCustomCooldown
|
|
|
|
window.addEventListener 'focus', QR.focus, true
|
|
window.addEventListener 'blur', QR.focus, true
|
|
# We don't receive blur events from captcha iframe.
|
|
$.on d, 'click', QR.focus
|
|
|
|
if $.engine is 'gecko'
|
|
nodes.pasteArea.hidden = false
|
|
new MutationObserver(QR.pasteFF).observe nodes.pasteArea, {childList: true}
|
|
|
|
# save selected post's data
|
|
items = ['thread', 'name', 'email', 'sub', 'com', 'filename']
|
|
i = 0
|
|
save = -> QR.selected.save @
|
|
while name = items[i++]
|
|
continue unless node = nodes[name]
|
|
event = if node.nodeName is 'SELECT' then 'change' else 'input'
|
|
$.on nodes[name], event, save
|
|
|
|
# XXX Blink and WebKit treat width and height of <textarea>s as min-width and min-height
|
|
if $.engine is 'gecko' and 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.generatePostableThreadsList()
|
|
QR.persona.init()
|
|
new QR.post true
|
|
QR.status()
|
|
QR.cooldown.setup()
|
|
QR.captcha.init()
|
|
|
|
$.add d.body, dialog
|
|
QR.captcha.setup()
|
|
|
|
# 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
|
|
|
|
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()
|
|
threadID = post.thread
|
|
thread = g.BOARD.threads[threadID]
|
|
if g.BOARD.ID is 'f' and threadID is 'new'
|
|
filetag = QR.nodes.flashTag.value
|
|
|
|
# prevent errors
|
|
if threadID is 'new'
|
|
threadID = null
|
|
if g.BOARD.ID is 'vg' 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.'
|
|
|
|
if QR.captcha.isEnabled and !err
|
|
captcha = QR.captcha.getOne()
|
|
err = 'No valid captcha.' unless captcha
|
|
|
|
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()
|
|
|
|
formData =
|
|
resto: threadID
|
|
name: post.name unless QR.forcedAnon
|
|
email: post.email
|
|
sub: post.sub unless QR.forcedAnon or threadID
|
|
com: post.com
|
|
upfile: post.file
|
|
filetag: filetag
|
|
spoiler: post.spoiler
|
|
textonly: textOnly
|
|
mode: 'regist'
|
|
pwd: QR.persona.pwd
|
|
|
|
options =
|
|
responseType: 'document'
|
|
withCredentials: true
|
|
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',
|
|
<%= html(
|
|
meta.name + ' encountered an error while posting. ' +
|
|
'[<a href="//4chan.org/banned" target="_blank">Banned?</a>] ' +
|
|
'[<a href="' + meta.faq + '#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean" target="_blank">More info</a>]'
|
|
) %>
|
|
extra =
|
|
form: $.formData formData
|
|
upCallbacks:
|
|
onload: ->
|
|
# Upload done, waiting for server response.
|
|
QR.req.isUploadFinished = true
|
|
QR.req.progress = '...'
|
|
QR.status()
|
|
onprogress: (e) ->
|
|
# Uploading...
|
|
QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
|
|
QR.status()
|
|
|
|
cb = (response) ->
|
|
if response?
|
|
if response.challenge?
|
|
extra.form.append 'recaptcha_challenge_field', response.challenge
|
|
extra.form.append 'recaptcha_response_field', response.response
|
|
else
|
|
extra.form.append 'g-recaptcha-response', response.response
|
|
QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra
|
|
QR.req.progress = '...'
|
|
|
|
if typeof captcha is 'function'
|
|
# Wait for captcha to be verified before submitting post.
|
|
QR.req =
|
|
progress: '...'
|
|
abort: -> cb = null
|
|
captcha (response) ->
|
|
if response
|
|
cb? response
|
|
else
|
|
delete QR.req
|
|
post.unlock()
|
|
QR.cooldown.auto = !!QR.captcha.captchas.length
|
|
QR.status()
|
|
else
|
|
cb captcha
|
|
|
|
# Starting to upload might take some time.
|
|
# Provide some feedback that we're starting to submit.
|
|
QR.status()
|
|
|
|
response: ->
|
|
{req} = QR
|
|
delete QR.req
|
|
|
|
post = QR.posts[0]
|
|
post.unlock()
|
|
|
|
resDoc = req.response
|
|
if ban = $ '.banType', resDoc # banned/warning
|
|
err = $.el 'span',
|
|
if ban.textContent.toLowerCase() is 'banned'
|
|
<%= html('You are banned on &{$(".board", resDoc)}! ;_;<br>Click <a href="//www.4chan.org/banned" target="_blank">here</a> to see the reason.') %>
|
|
else
|
|
<%= html('You were issued a warning on &{$(".board", resDoc)} as &{$(".nameBlock", resDoc)}.<br>Reason: &{$(".reason", resDoc)}') %>
|
|
else if err = resDoc.getElementById 'errmsg' # error!
|
|
$('a', err)?.target = '_blank' # duplicate image link
|
|
else if resDoc.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 = $.el 'span',
|
|
<%= html('You mistyped the CAPTCHA, or the CAPTCHA malfunctioned [<a href="https://www.4chan-x.net/captchas.html" target="_blank">complain here</a>].') %>
|
|
else if /expired/i.test err.textContent
|
|
err = 'This CAPTCHA is no longer valid because it has expired.'
|
|
# 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.addDelay post, 2
|
|
else if err.textContent and (m = err.textContent.match /wait\s+(\d+)\s+second/i) and !/duplicate/i.test err.textContent
|
|
QR.cooldown.auto = if QR.captcha.isEnabled
|
|
!!QR.captcha.captchas.length
|
|
else
|
|
true
|
|
QR.cooldown.addDelay post, +m[1]
|
|
QR.captcha.setup (d.activeElement is QR.nodes.status)
|
|
else # stop auto-posting
|
|
QR.cooldown.auto = false
|
|
QR.status()
|
|
QR.error err
|
|
return
|
|
|
|
h1 = $ 'h1', resDoc
|
|
QR.cleanNotifications()
|
|
|
|
if Conf['Posting Success Notifications']
|
|
QR.notifications.push new Notice 'success', h1.textContent, 5
|
|
|
|
[_, 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
|
|
|
|
# Post/upload confirmed as successful.
|
|
$.event 'QRPostSuccessful', {
|
|
boardID: g.BOARD.ID
|
|
threadID
|
|
postID
|
|
}
|
|
$.event 'QRPostSuccessful_', {boardID: g.BOARD.ID, threadID, postID}
|
|
|
|
# Enable auto-posting if we have stuff left to post, disable it otherwise.
|
|
postsCount = QR.posts.length - 1
|
|
QR.cooldown.auto = postsCount and isReply
|
|
|
|
lastPostToThread = not (do -> return true for p in QR.posts[1..] when p.thread is post.thread)
|
|
|
|
unless Conf['Persistent QR'] or postsCount
|
|
QR.close()
|
|
else
|
|
post.rm()
|
|
QR.captcha.setup(d.activeElement is QR.nodes.status)
|
|
|
|
QR.cooldown.add threadID, postID
|
|
|
|
URL = if threadID is postID # new thread
|
|
"#{window.location.origin}/#{g.BOARD}/thread/#{threadID}"
|
|
else if g.VIEW is 'index' and lastPostToThread and Conf['Open Post in New Tab'] # replying from the index
|
|
"#{window.location.origin}/#{g.BOARD}/thread/#{threadID}#p#{postID}"
|
|
|
|
if URL
|
|
open = if Conf['Open Post in New Tab'] or postsCount
|
|
-> $.open URL
|
|
else
|
|
-> window.location = URL
|
|
|
|
if threadID is postID
|
|
# XXX 4chan sometimes responds before the thread exists.
|
|
QR.waitForThread URL, open
|
|
else
|
|
open()
|
|
|
|
QR.status()
|
|
|
|
waitForThread: (url, cb) ->
|
|
attempts = 0
|
|
check = ->
|
|
$.ajax url,
|
|
onloadend: ->
|
|
attempts++
|
|
if attempts >= 5 or @status is 200
|
|
cb()
|
|
else
|
|
setTimeout check, attempts * $.SECOND
|
|
,
|
|
type: 'HEAD'
|
|
check()
|
|
|
|
abort: ->
|
|
if QR.req and !QR.req.isUploadFinished
|
|
QR.req.abort()
|
|
delete QR.req
|
|
QR.posts[0].unlock()
|
|
QR.cooldown.auto = false
|
|
QR.notifications.push new Notice 'info', 'QR upload aborted.', 5
|
|
QR.status()
|