QR = mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'] init: -> return if !Conf['Quick Reply'] @db = new DataBoard 'yourPosts' @posts = [] $.globalEval 'document.documentElement.dataset.jsEnabled = true;' noscript = Conf['Force Noscript Captcha'] or !doc.dataset.jsEnabled @captcha = Captcha[if noscript then 'noscript' else 'v2'] if Conf['QR Shortcut'] sc = $.el 'a', className: "qr-shortcut fa fa-comment-o #{unless Conf['Persistent QR'] then 'disabled' else ''}" 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() $.rmClass @, 'disabled' else QR.close() $.addClass @, 'disabled' Header.addShortcut sc if Conf['Hide Original Post Form'] $.addClass doc, 'hide-original-post-form' if !doc.dataset.jsEnabled # Prevent unnecessary loading of fallback iframe. $.onExists doc, '#postForm noscript', true, $.rm $.on d, '4chanXInitFinished', @initReady window.addEventListener 'focus', @focus, true window.addEventListener 'blur', @focus, true Post.callbacks.push name: 'Quick Reply' cb: @node initReady: -> $.off d, '4chanXInitFinished', @initReady QR.postingIsEnabled = !!$.id 'postForm' return unless QR.postingIsEnabled link = $.el 'h1', className: "qr-link-container" $.extend link, <%= html('${(g.VIEW === "thread") ? "Reply to Thread" : "Start a Thread"}') %> QR.link = link.firstElementChild $.on link.firstChild, 'click', -> $.event 'CloseMenu' QR.open() QR.nodes.com.focus() if Conf['QR Shortcut'] $.rmClass $('.qr-shortcut'), 'disabled' if Conf['Bottom QR Link'] and g.VIEW is 'thread' linkBot = $.el 'div', className: "brackets-wrap qr-link-container-bottom" $.extend linkBot, <%= html('Reply to Thread') %> $.on linkBot.firstElementChild, 'click', -> $.event 'CloseMenu' QR.open() QR.nodes.com.focus() if Conf['QR Shortcut'] $.rmClass $('.qr-shortcut'), 'disabled' $.prepend $('.navLinksBot'), linkBot $.before $.id('togglePostFormLink'), link $.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() 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' if Conf['QR Shortcut'] $.toggleClass $('.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 -> return unless QR.nodes focus = d.activeElement and ( QR.nodes.el.contains(d.activeElement) or d.activeElement.nodeName is 'IFRAME' and /^https:\/\/www\.google\.com\/recaptcha\//.test(d.activeElement.src) ) if $.hasClass(QR.nodes.el, 'autohide') and focus isnt $.hasClass(QR.nodes.el, 'focus') QR.captcha[if focus then 'setup' else 'destroy']() $[if focus then 'addClass' else 'rmClass'] QR.nodes.el, 'focus' hide: -> QR.captcha.destroy() d.activeElement.blur() $.addClass QR.nodes.el, 'autohide' QR.nodes.autohide.checked = true unhide: -> QR.captcha.setup() if $.hasClass(QR.nodes.el, 'autohide') and !$.hasClass(QR.nodes.el, 'focus') $.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 QR.captcha.setup true QR.captcha.notify el else QR.notify el alert el.textContent if d.hidden notify: (el) -> notice = new Notice 'warning', el unless Header.areNotificationsEnabled and d.hidden QR.notifications.push notice else notif = new Notification el.textContent, body: el.textContent icon: Favicon.logo notif.onclick = -> window.focus() <% if (type === 'crx') { %> # 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 if Conf['QR Shortcut'] $.rmClass $('.qr-shortcut'), 'disabled' characterCount: -> counter = QR.nodes.charCount count = QR.nodes.com.textLength counter.textContent = count counter.hidden = count < 1000 (if count > 1500 then $.addClass else $.rmClass) counter, 'warning' 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' handleUrl: -> url = prompt 'Enter a URL:' return if url is null 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, i in files QR.handleFile file, i, files.length $.addClass QR.nodes.el, 'dump' unless files.length is 1 handleFile: (file, index, nfiles) -> isSingle = nfiles is 1 if /^text\//.test file.type if isSingle post = QR.selected else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).com post = new QR.post() post.pasteText file return unless file.type in QR.mimeTypes QR.error "#{file.name}: Unsupported file type." return unless isSingle max = QR.nodes.fileInput.max max = Math.min(max, QR.max_size_video) if /^video\//.test file.type if file.size > max QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})." return unless isSingle isNewPost = false if isSingle post = QR.selected else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).file isNewPost = true post = new QR.post() QR.checkDimensions file, (pass, el) -> if pass or isSingle post.setFile file, el else if isNewPost post.rm() URL.revokeObjectURL el.src if el checkDimensions: (file, cb) -> if /^image\//.test file.type img = new Image() img.onload = -> {height, width} = img pass = true if height > QR.max_height or width > QR.max_width QR.error "#{file.name}: Image too large (image: #{height}x#{width}px, max: #{QR.max_height}x#{QR.max_width}px)" pass = false if height < QR.min_height or width < QR.min_width QR.error "#{file.name}: Image too small (image: #{height}x#{width}px, min: #{QR.min_height}x#{QR.min_width}px)" pass = false cb pass, img img.onerror = -> cb false, null img.src = URL.createObjectURL file else if /^video\//.test file.type video = $.el 'video' $.on video, 'loadeddata', -> return unless cb {videoHeight, videoWidth, duration} = video max_height = Math.min(QR.max_height, QR.max_height_video) max_width = Math.min(QR.max_width, QR.max_width_video) pass = true if videoHeight > max_height or videoWidth > max_width QR.error "#{file.name}: Video too large (video: #{videoHeight}x#{videoWidth}px, max: #{max_height}x#{max_width}px)" pass = false if videoHeight < QR.min_height or videoWidth < QR.min_width QR.error "#{file.name}: Video too small (video: #{videoHeight}x#{videoWidth}px, min: #{QR.min_height}x#{QR.min_width}px)" pass = false unless isFinite duration QR.error "#{file.name}: Video lacks duration metadata (try remuxing)" pass = false else if duration > QR.max_duration_video QR.error "#{file.name}: Video too long (video: #{duration}s, max: #{QR.max_duration_video}s)" pass = false if video.mozHasAudio or video.webkitAudioDecodedByteCount QR.error "#{file.name}: Audio not allowed" pass = false cb pass, video cb = null $.on video, 'error', -> return unless cb if file.type in QR.mimeTypes # only report error here if we should have been able to play the video # otherwise "unsupported type" should already have been shown QR.error "#{file.name}: Video appears corrupt" URL.revokeObjectURL file cb false, null cb = null video.src = URL.createObjectURL file else cb true, null openFileInput: (e) -> e.stopPropagation() if e.shiftKey and e.type is 'click' return QR.selected.rmFile() if (e.ctrlKey or e.metaKey) and e.type is 'click' $.addClass QR.nodes.filename, 'edit' QR.nodes.filename.focus() return $.on QR.nodes.filename, 'blur', -> $.rmClass QR.nodes.filename, 'edit' return if e.target.nodeName is 'INPUT' or (e.keyCode and e.keyCode not in [32, 13]) or e.ctrlKey e.preventDefault() QR.nodes.fileInput.click() generatePostableThreadsList: -> return unless QR.nodes list = QR.nodes.thread options = [list.firstChild] for thread in g.BOARD.threads.keys options.push $.el 'option', value: thread textContent: "Thread No.#{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 '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 'filename', '#qr-filename' setNode 'fileContainer', '#qr-filename-container' setNode 'fileRM', '#qr-filerm' setNode 'fileExtras', '#qr-extras-container' setNode 'spoiler', '#qr-file-spoiler' setNode 'spoilerPar', '#qr-spoiler-label' setNode 'status', '[type=submit]' setNode 'fileInput', '[type=file]' 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 QR.max_size_video = 3145728 QR.max_width_video = QR.max_height_video = 2048 QR.max_duration_video = 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 g.BOARD.ID is 'f' and g.VIEW isnt 'thread' nodes.flashTag = $.el 'select', name: 'filetag' $.extend nodes.flashTag, <%= html( '' + '' + '' + '' + '' + '' + '' ) %> nodes.flashTag.dataset.default = '4' $.add nodes.form, nodes.flashTag QR.flagsInput() $.on nodes.filename.parentNode, 'click keydown', QR.openFileInput $.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.fileExtras, 'click', (e) -> e.stopPropagation() $.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click() $.on nodes.fileInput, 'change', QR.handleFiles # save selected post's data items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag'] 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 <% 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.generatePostableThreadsList() QR.persona.init() new QR.post true QR.status() QR.cooldown.init() QR.captcha.init() $.add d.body, dialog QR.captcha.setup() unless Conf['Persistent QR'] and Conf['Auto Hide QR'] # 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 flags: -> select = $.el 'select', name: 'flag' className: 'flagSelector' fn = (val) -> $.add select, $.el 'option', value: val[0] textContent: val[1] fn flag for flag in [ ['0', 'None'] ['US', 'American'] ['KP', 'Best Korean'] ['BL', 'Black Nationalist'] ['CM', 'Communist'] ['CF', 'Confederate'] ['RE', 'Conservative'] ['EU', 'European'] ['GY', 'Gay'] ['PC', 'Hippie'] ['IL', 'Israeli'] ['DM', 'Liberal'] ['RP', 'Libertarian'] ['MF', 'Muslim'] ['NZ', 'Nazi'] ['OB', 'Obama'] ['PR', 'Pirate'] ['RB', 'Rebel'] ['TP', 'Tea Partier'] ['TX', 'Texan'] ['TR', 'Tree Hugger'] ['WP', 'White Supremacist'] ] select flagsInput: -> {nodes} = QR return if not nodes if nodes.flag $.rm nodes.flag delete nodes.flag if false flag = QR.flags() flag.dataset.name = 'flag' flag.dataset.default = '0' nodes.flag = flag $.add nodes.form, flag 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' and g.VIEW isnt 'thread' filetag = QR.nodes.flashTag.value threadID = post.thread thread = g.BOARD.threads[threadID] # 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 flag: post.flag 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( '4chan X encountered an error while posting. ' + '[Banned?] ' + '[More info]' ) %> extra = form: $.formData formData 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() cb = (response) -> extra.form.append 'g-recaptcha-response', response if 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)}! ;_;
Click here to see the reason.') %> else <%= html('You were issued a warning on &{$(".board", resDoc)} as &{$(".nameBlock", resDoc)}.
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 = 'You seem to have mistyped the CAPTCHA.' 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 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', { 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 if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount notif = new Notification 'Quick reply warning', body: "You are running low on cached captchas. Cache count: #{captchasCount}." icon: Favicon.logo notif.onclick = -> QR.open() window.focus() QR.captcha.setup true notif.onshow = -> setTimeout -> notif.close() , 7 * $.SECOND unless Conf['Persistent QR'] or postsCount QR.close() else post.rm() QR.captcha.setup (d.activeElement is QR.nodes.status) QR.cooldown.add req.uploadEndTime, threadID, postID URL = if threadID is postID # new thread "#{window.location.origin}/#{g.BOARD}/thread/#{threadID}" else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index "#{window.location.origin}/#{g.BOARD}/thread/#{threadID}#p#{postID}" if URL if Conf['Open Post in New Tab'] or postsCount $.open URL else window.location = URL QR.status() 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()