Merge branch 'v3' into Av2

Conflicts:
	appchan-x.meta.js
	appchan-x.user.js
	package.json
	src/features.coffee
	src/main.coffee
This commit is contained in:
Zixaphir 2013-04-08 07:50:39 -07:00
commit 0a5bd72be0
15 changed files with 2252 additions and 1476 deletions

View File

@ -1,4 +1,10 @@
# 3.0.0 ### 3.0.1 - *2013-04-08*
- Added the possibility to combine board-list toggle and custom text.
- Added Reply Navigation back in, disabled by default.
- Fixed Thread Hiding initialization error.
# 3.0.0 - *2013-04-07*
**Major rewrite of 4chan X.** **Major rewrite of 4chan X.**
@ -7,10 +13,16 @@ Header:
- The board list can be customized. - The board list can be customized.
- The Header can be automatically hidden. - The Header can be automatically hidden.
Extension-related changes for Chrome and Opera:
- Installing and updating is now pain-free on Chrome.
- Settings will persist on different subdomains and protocols (HTTP/HTTPS).
- Settings will persist in Incognito on Chrome.
- Clearing your cookies won't erase your settings anymore.
- Fixed Chrome's install warning saying that 4chan X would run on all web sites.
Egocentrism: Egocentrism:
- `(You)` will be added to quotes linking to your posts. - `(You)` will be added to quotes linking to your posts.
- The Unread tab icon will indicate new unread posts quoting you with an exclamation mark. - The Unread tab icon will indicate new unread posts quoting you with an exclamation mark.
- Delete links in the post menu will only appear for your posts.
Quick Reply changes: Quick Reply changes:
- Opening text files will insert their content in the comment field. - Opening text files will insert their content in the comment field.
@ -21,11 +33,12 @@ Quick Reply changes:
- Closing the QR while uploading will abort the upload and won't close the QR anymore. - Closing the QR while uploading will abort the upload and won't close the QR anymore.
- Creating threads outside of the index is now possible. - Creating threads outside of the index is now possible.
- Selection-to-quote also applies to selected text inside the post, not just inside the comment. - Selection-to-quote also applies to selected text inside the post, not just inside the comment.
- Added support for thread creation in the catalog.
- Added thumbnailing support for Opera. - Added thumbnailing support for Opera.
Image Expansion changes: Image Expansion changes:
- The toggle and settings are now located in the Header's shortcuts and menu. - The toggle and settings are now located in the Header's shortcuts and menu.
- There is now a setting to allow expanding spoilers. - Expanding spoilers along with all non-spoiler images is now optional, and disabled by default.
- Expanding OP images won't squish replies anymore. - Expanding OP images won't squish replies anymore.
Thread Updater changes: Thread Updater changes:

View File

@ -3,7 +3,7 @@
1. Make sure both your **browser** and **4chan X** are up to date. 1. Make sure both your **browser** and **4chan X** are up to date.
2. Disable your other extensions & scripts to identify conflicts. 2. Disable your other extensions & scripts to identify conflicts.
3. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information: 3. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information:
1. Precise steps to reproduce the problem. 1. Precise steps to reproduce the problem, with the expected and actual results.
2. Console errors, if any. 2. Console errors, if any.
3. Browser version. 3. Browser version.
4. Your exported settings. 4. Your exported settings.
@ -21,7 +21,7 @@ Open your console with:
- Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`. - Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`.
- Clone 4chan X. - Clone 4chan X.
- `cd` into it. - `cd` into it.
- Install 4chan X dependencies with `npm install`. - Install/Update 4chan X dependencies with `npm install`.
### Build ### Build

View File

@ -20,6 +20,7 @@ module.exports = (grunt) ->
'src/features.coffee' 'src/features.coffee'
'src/qr.coffee' 'src/qr.coffee'
'src/report.coffee' 'src/report.coffee'
'src/databoard.coffee'
'src/main.coffee' 'src/main.coffee'
] ]
dest: 'tmp/script.coffee' dest: 'tmp/script.coffee'
@ -75,10 +76,10 @@ module.exports = (grunt) ->
command: -> command: ->
release = "#{pkg.meta.name} v#{pkg.version}" release = "#{pkg.meta.name} v#{pkg.version}"
return [ return [
"git checkout #{pkg.meta.mainBranch}" 'git checkout ' + pkg.meta.mainBranch,
"git commit -am 'Release #{release}.'" 'git commit -am "Release ' + release + '."',
"git tag -a #{pkg.version} -m '#{release}.'" 'git tag -a ' + pkg.version + ' -m "' + release + '."',
"git tag -af stable -m '#{release}.'" 'git tag -af stable-v3 -m "' + release + '."'
].join(' && '); ].join(' && ');
stdout: true stdout: true

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,18 @@ master
GIF thumbnail replacement, unlike Auto-GIF, actually works in /gif/ and /wsg/. GIF thumbnail replacement, unlike Auto-GIF, actually works in /gif/ and /wsg/.
Various little performance and readability tweaks. Various little performance and readability tweaks.
2.39.3
- Mayhem
Add /fa/ and /s4s/ archive redirection.
2.39.2
- Mayhem
Fix importing settings containing unicode characters.
2.39.1
- Mayhem
Add /gd/, /out/, /vp/ and /vr/ archive redirection.
2.39.0 2.39.0
- Queue - Queue
Fix rare bug in Relative Post Dates. Fix rare bug in Relative Post Dates.

View File

@ -14,7 +14,7 @@
margin: 0; margin: 0;
padding: 2px 4px 3px; padding: 2px 4px 3px;
outline: none; outline: none;
-webkit-transition: color .25s, border-color .25s, -webkit-flex .25s; transition: color .25s, border-color .25s, -webkit-flex .25s;
transition: color .25s, border-color .25s, flex .25s; transition: color .25s, border-color .25s, flex .25s;
} }
.field::-moz-placeholder, .field::-moz-placeholder,
@ -107,7 +107,6 @@ a[href="javascript:;"] {
display: flex; display: flex;
padding: 3px 4px 4px; padding: 3px 4px 4px;
position: relative; position: relative;
-webkit-transition: all .1s .05s ease-in-out;
transition: all .1s .05s ease-in-out; transition: all .1s .05s ease-in-out;
} }
#board-list { #board-list {
@ -120,7 +119,6 @@ a[href="javascript:;"] {
margin-bottom: -1em; margin-bottom: -1em;
-webkit-transform: translateY(-100%); -webkit-transform: translateY(-100%);
transform: translateY(-100%); transform: translateY(-100%);
-webkit-transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19); transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
} }
#toggle-header-bar { #toggle-header-bar {
@ -175,7 +173,6 @@ a[href="javascript:;"] {
width: 500px; width: 500px;
max-width: 100%; max-width: 100%;
position: relative; position: relative;
-webkit-transition: all .25s ease-in-out;
transition: all .25s ease-in-out; transition: all .25s ease-in-out;
} }
.notification.error { .notification.error {
@ -346,6 +343,9 @@ a[href="javascript:;"] {
#updater:not(:hover) > div:not(.move) { #updater:not(:hover) > div:not(.move) {
display: none; display: none;
} }
#updater input[type="button"] {
width: 100%;
}
.new { .new {
color: limegreen; color: limegreen;
} }
@ -495,8 +495,9 @@ a[href="javascript:;"] {
} }
/* QR */ /* QR */
.hide-original-post-form #postForm, :root.hide-original-post-form #postForm,
.hide-original-post-form .postingMode, :root.hide-original-post-form .postingMode,
:root.hide-original-post-form #togglePostForm,
#qr.autohide:not(:hover) > form { #qr.autohide:not(:hover) > form {
display: none; display: none;
} }
@ -545,11 +546,10 @@ a[href="javascript:;"] {
flex: 1; flex: 1;
} }
.persona .field:focus { .persona .field:focus {
-webkit-flex: 4; -webkit-flex: 3;
flex: 4; flex: 3;
} }
#dump-button { #dump-button {
background: -webkit-linear-gradient(#EEE, #CCC);
background: linear-gradient(#EEE, #CCC); background: linear-gradient(#EEE, #CCC);
border: 1px solid #CCC; border: 1px solid #CCC;
margin: 0; margin: 0;
@ -558,11 +558,9 @@ a[href="javascript:;"] {
width: 30px; width: 30px;
} }
#dump-button:hover, #dump-button:focus { #dump-button:hover, #dump-button:focus {
background: -webkit-linear-gradient(#FFF, #DDD);
background: linear-gradient(#FFF, #DDD); background: linear-gradient(#FFF, #DDD);
} }
#dump-button:active, .dump #dump-button:not(:hover):not(:focus) { #dump-button:active, .dump #dump-button:not(:hover):not(:focus) {
background: -webkit-linear-gradient(#CCC, #DDD);
background: linear-gradient(#CCC, #DDD); background: linear-gradient(#CCC, #DDD);
} }
.gecko #dump-button { .gecko #dump-button {
@ -614,7 +612,6 @@ a[href="javascript:;"] {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
text-shadow: 0 1px 1px #000; text-shadow: 0 1px 1px #000;
-webkit-transition: opacity .25s ease-in-out;
transition: opacity .25s ease-in-out; transition: opacity .25s ease-in-out;
vertical-align: top; vertical-align: top;
white-space: pre; white-space: pre;

View File

@ -62,11 +62,6 @@ $.extend $,
$.off d, 'DOMContentLoaded', cb $.off d, 'DOMContentLoaded', cb
fc() fc()
$.on d, 'DOMContentLoaded', cb $.on d, 'DOMContentLoaded', cb
sync: (key, cb) ->
key = "#{g.NAMESPACE}#{key}"
$.on window, 'storage', (e) ->
if e.key is key
cb JSON.parse e.newValue
formData: (form) -> formData: (form) ->
if form instanceof HTMLFormElement if form instanceof HTMLFormElement
return new FormData form return new FormData form
@ -81,15 +76,15 @@ $.extend $,
fd.append key, val fd.append key, val
fd fd
ajax: (url, callbacks, opts={}) -> ajax: (url, callbacks, opts={}) ->
{type, headers, upCallbacks, form} = opts {type, cred, headers, upCallbacks, form, sync} = opts
r = new XMLHttpRequest() r = new XMLHttpRequest()
type or= form and 'post' or 'get' type or= form and 'post' or 'get'
r.open type, url, true r.open type, url, !sync
for key, val of headers for key, val of headers
r.setRequestHeader key, val r.setRequestHeader key, val
$.extend r, callbacks $.extend r, callbacks
$.extend r.upload, upCallbacks $.extend r.upload, upCallbacks
r.withCredentials = type is 'post' r.withCredentials = cred
r.send form r.send form
r r
cache: do -> cache: do ->
@ -101,12 +96,13 @@ $.extend $,
else else
req.callbacks.push cb req.callbacks.push cb
return return
rm = -> delete reqs[url]
req = $.ajax url, req = $.ajax url,
onload: -> onload: (e) ->
cb.call @ for cb in @callbacks cb.call @, e for cb in @callbacks
delete @callbacks delete @callbacks
onabort: -> delete reqs[url] onabort: rm
onerror: -> delete reqs[url] onerror: rm
req.callbacks = [cb] req.callbacks = [cb]
reqs[url] = req reqs[url] = req
cb: cb:
@ -244,19 +240,43 @@ $.extend $,
# Round to an integer otherwise. # Round to an integer otherwise.
Math.round size Math.round size
"#{size} #{['B', 'KB', 'MB', 'GB'][unit]}" "#{size} #{['B', 'KB', 'MB', 'GB'][unit]}"
syncing: {}
sync: do ->
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
delete: (keys) -> chrome.storage.onChanged.addListener (changes) ->
chrome.storage.sync.remove keys for key of changes
get: (key, defaultVal) -> if cb = $.syncing[key]
if val = localStorage.getItem g.NAMESPACE + key cb changes[key].newValue
JSON.parse val return
else (key, cb) -> $.syncing[key] = cb
defaultVal <% } else { %>
set: (key, val) -> window.addEventListener 'storage', (e) ->
if cb = $.syncing[e.key]
cb JSON.parse e.newValue
, false
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
<% } %>
item: (key, val) ->
item = {} item = {}
item[key] = val item[key] = val
chrome.storage.sync.set item item
<% if (type === 'crx') { %>
# https://developer.chrome.com/extensions/storage.html
delete: (keys) ->
chrome.storage.sync.remove keys
get: (key, val, cb) ->
if typeof cb is 'function'
items = $.item key, val
else
items = key
cb = val
chrome.storage.sync.get items, cb
set: (key, val) ->
items = if typeof key is 'string'
$.item key, val
else
key
chrome.storage.sync.set items
<% } else if (type === 'userjs') { %> <% } else if (type === 'userjs') { %>
do -> do ->
# http://www.opera.com/docs/userjs/specs/#scriptstorage # http://www.opera.com/docs/userjs/specs/#scriptstorage
@ -275,19 +295,35 @@ do ->
localStorage.removeItem key localStorage.removeItem key
delete scriptStorage[key] delete scriptStorage[key]
return return
$.get = (key, defaultVal) -> $.get = (key, val, cb) ->
if val = scriptStorage[g.NAMESPACE + key] if typeof cb is 'function'
JSON.parse val items = $.item key, val
else else
defaultVal items = key
$.set = (key, val) -> cb = val
key = g.NAMESPACE + key $.queueTask ->
val = JSON.stringify val for key of items
# for `storage` events if val = scriptStorage[g.NAMESPACE + key]
localStorage.setItem key, val items[key] = JSON.parse val
scriptStorage[key] = val cb items
$.set = do ->
set = (key, val) ->
key = g.NAMESPACE + key
val = JSON.stringify val
if key of $.syncing
# for `storage` events
localStorage.setItem key, val
scriptStorage[key] = val
(keys, val) ->
if typeof keys is 'string'
set keys, val
return
for key, val of keys
set key, val
return
<% } else { %> <% } else { %>
delete: (key) -> # http://wiki.greasespot.net/Main_Page
delete: (keys) ->
unless keys instanceof Array unless keys instanceof Array
keys = [keys] keys = [keys]
for key in keys for key in keys
@ -295,15 +331,30 @@ do ->
localStorage.removeItem key localStorage.removeItem key
GM_deleteValue key GM_deleteValue key
return return
get: (key, defaultVal) -> get: (key, val, cb) ->
if val = GM_getValue g.NAMESPACE + key if typeof cb is 'function'
JSON.parse val items = $.item key, val
else else
defaultVal items = key
set: (key, val) -> cb = val
key = g.NAMESPACE + key $.queueTask ->
val = JSON.stringify val for key of items
# for `storage` events if val = GM_getValue g.NAMESPACE + key
localStorage.setItem key, val items[key] = JSON.parse val
GM_setValue key, val cb items
set: do ->
set = (key, val) ->
key = g.NAMESPACE + key
val = JSON.stringify val
if key of $.syncing
# for `storage` events
localStorage.setItem key, val
GM_setValue key, val
(keys, val) ->
if typeof keys is 'string'
set keys, val
return
for key, val of keys
set key, val
return
<% } %> <% } %>

View File

@ -1,13 +1,12 @@
UI = do -> UI = do ->
dialog = (id, position, html) -> dialog = (id, position, html) ->
el = d.createElement 'div' el = $.el 'div',
el.className = 'dialog' className: 'dialog'
el.innerHTML = html innerHTML: html
el.id = id id: id
el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position
move = el.querySelector '.move' move = $ '.move', el
move.addEventListener 'touchstart', dragstart, false $.on move, 'touchstart mousedown', dragstart
move.addEventListener 'mousedown', dragstart, false
el el
@ -103,8 +102,7 @@ UI = do ->
$.rm currentMenu $.rm currentMenu
currentMenu = null currentMenu = null
lastToggledButton = null lastToggledButton = null
$.off d, 'click', @close $.off d, 'click CloseMenu', @close
$.off d, 'CloseMenu', @close
findNextEntry: (entry, direction) -> findNextEntry: (entry, direction) ->
entries = [entry.parentNode.children...] entries = [entry.parentNode.children...]
@ -156,18 +154,14 @@ UI = do ->
eRect = entry.getBoundingClientRect() eRect = entry.getBoundingClientRect()
cHeight = doc.clientHeight cHeight = doc.clientHeight
cWidth = doc.clientWidth cWidth = doc.clientWidth
if eRect.top + sRect.height < cHeight [top, bottom] = if eRect.top + sRect.height < cHeight
top = '0px' ['0px', 'auto']
bottom = 'auto'
else else
top = 'auto' ['auto', '0px']
bottom = '0px' [left, right] = if eRect.right + sRect.width < cWidth
if eRect.right + sRect.width < cWidth ['100%', 'auto']
left = '100%'
right = 'auto'
else else
left = 'auto' ['auto', '100%']
right = '100%'
{style} = submenu {style} = submenu
style.top = top style.top = top
style.bottom = bottom style.bottom = bottom
@ -197,14 +191,13 @@ UI = do ->
dragstart = (e) -> dragstart = (e) ->
if e.type is 'mousedown' and e.button isnt 0 # not LMB return if e.type is 'mousedown' and e.button isnt 0 # not LMB
return
# prevent text selection # prevent text selection
e.preventDefault() e.preventDefault()
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
if isTouching = e.type is 'touchstart' if isTouching = e.type is 'touchstart'
e = e.changedTouches[e.changedTouches.length - 1] e = e.changedTouches[e.changedTouches.length - 1]
# distance from pointer to el edge is constant; calculate it here. # distance from pointer to el edge is constant; calculate it here.
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
rect = el.getBoundingClientRect() rect = el.getBoundingClientRect()
screenHeight = doc.clientHeight screenHeight = doc.clientHeight
screenWidth = doc.clientWidth screenWidth = doc.clientWidth
@ -223,14 +216,13 @@ UI = do ->
o.identifier = e.identifier o.identifier = e.identifier
o.move = touchmove.bind o o.move = touchmove.bind o
o.up = touchend.bind o o.up = touchend.bind o
d.addEventListener 'touchmove', o.move, false $.on d, 'touchmove', o.move
d.addEventListener 'touchend', o.up, false $.on d, 'touchend touchcancel', o.up
d.addEventListener 'touchcancel', o.up, false
else # mousedown else # mousedown
o.move = drag.bind o o.move = drag.bind o
o.up = dragend.bind o o.up = dragend.bind o
d.addEventListener 'mousemove', o.move, false $.on d, 'mousemove', o.move
d.addEventListener 'mouseup', o.up, false $.on d, 'mouseup', o.up
touchmove = (e) -> touchmove = (e) ->
for touch in e.changedTouches for touch in e.changedTouches
if touch.identifier is @identifier if touch.identifier is @identifier
@ -240,33 +232,29 @@ UI = do ->
{clientX, clientY} = e {clientX, clientY} = e
left = clientX - @dx left = clientX - @dx
left = left = if left < 10
if left < 10 0
0 else if @width - left < 10
else if @width - left < 10 null
null else
else left / @screenWidth * 100 + '%'
left / @screenWidth * 100 + '%'
top = clientY - @dy top = clientY - @dy
top = top = if top < 10
if top < 10 0
0 else if @height - top < 10
else if @height - top < 10 null
null else
else top / @screenHeight * 100 + '%'
top / @screenHeight * 100 + '%'
right = right = if left is null
if left is null 0
0 else
else null
null bottom = if top is null
bottom = 0
if top is null else
0 null
else
null
{style} = @ {style} = @
style.left = left style.left = left
@ -280,12 +268,11 @@ UI = do ->
return return
dragend = -> dragend = ->
if @isTouching if @isTouching
d.removeEventListener 'touchmove', @move, false $.off d, 'touchmove', @move
d.removeEventListener 'touchend', @up, false $.off d, 'touchend touchcancel', @up
d.removeEventListener 'touchcancel', @up, false
else # mouseup else # mouseup
d.removeEventListener 'mousemove', @move, false $.off d, 'mousemove', @move
d.removeEventListener 'mouseup', @up, false $.off d, 'mouseup', @up
localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) -> hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) ->
@ -294,7 +281,7 @@ UI = do ->
el: el el: el
style: el.style style: el.style
cb: cb cb: cb
endEvents: endEvents.split ' ' endEvents: endEvents
latestEvent: latestEvent latestEvent: latestEvent
clientHeight: doc.clientHeight clientHeight: doc.clientHeight
clientWidth: doc.clientWidth clientWidth: doc.clientWidth
@ -302,47 +289,39 @@ UI = do ->
o.hover = hover.bind o o.hover = hover.bind o
o.hoverend = hoverend.bind o o.hoverend = hoverend.bind o
asap = -> $.asap ->
if asapTest() !el.parentNode or asapTest()
o.hover o.latestEvent , ->
else o.hover o.latestEvent if el.parentNode
o.timeout = setTimeout asap, 25
asap()
for event in o.endEvents $.on root, endEvents, o.hoverend
root.addEventListener event, o.hoverend, false $.on root, 'mousemove', o.hover
root.addEventListener 'mousemove', o.hover, false
hover = (e) -> hover = (e) ->
@latestEvent = e @latestEvent = e
height = @el.offsetHeight height = @el.offsetHeight
{clientX, clientY} = e {clientX, clientY} = e
top = clientY - 120 top = clientY - 120
top = top = if @clientHeight <= height or top <= 0
if @clientHeight <= height or top <= 0 0
0 else if top + height >= @clientHeight
else if top + height >= @clientHeight @clientHeight - height
@clientHeight - height
else
top
if clientX <= @clientWidth - 400
left = clientX + 45 + 'px'
right = null
else else
left = null top
right = @clientWidth - clientX + 45 + 'px'
[left, right] = if clientX <= @clientWidth - 400
[clientX + 45 + 'px', null]
else
[null, @clientWidth - clientX + 45 + 'px']
{style} = @ {style} = @
style.top = top + 'px' style.top = top + 'px'
style.left = left style.left = left
style.right = right style.right = right
hoverend = -> hoverend = ->
@el.parentNode.removeChild @el $.rm @el
for event in @endEvents $.off @root, @endEvents, @hoverend
@root.removeEventListener event, @hoverend, false $.off @root, 'mousemove', @hover
@root.removeEventListener 'mousemove', @hover, false
clearTimeout @timeout
@cb.call @ if @cb @cb.call @ if @cb

View File

@ -20,10 +20,10 @@
"grunt": "~0.4.1", "grunt": "~0.4.1",
"grunt-bump": "~0.0.0", "grunt-bump": "~0.0.0",
"grunt-contrib-clean": "~0.4.0", "grunt-contrib-clean": "~0.4.0",
"grunt-contrib-coffee": "~0.6.4", "grunt-contrib-coffee": "~0.6.5",
"grunt-contrib-compress": "~0.4.5", "grunt-contrib-compress": "~0.4.7",
"grunt-contrib-concat": "~0.1.3", "grunt-contrib-concat": "~0.1.3",
"grunt-contrib-copy": "~0.4.0", "grunt-contrib-copy": "~0.4.1",
"grunt-contrib-watch": "~0.3.1", "grunt-contrib-watch": "~0.3.1",
"grunt-exec": "~0.4.0" "grunt-exec": "~0.4.0"
}, },

View File

@ -49,6 +49,10 @@ Config =
false false
'Add buttons to navigate between threads.' 'Add buttons to navigate between threads.'
] ]
'Reply Navigation': [
false
'Add buttons to navigate to top / bottom of thread.'
]
'Check for Updates': [ 'Check for Updates': [
true true
'Check for updated versions of <%= meta.name %>.' 'Check for updated versions of <%= meta.name %>.'
@ -232,7 +236,7 @@ Config =
'Add quote backlinks.' 'Add quote backlinks.'
] ]
'OP Backlinks': [ 'OP Backlinks': [
false true
'Add backlinks to the OP.' 'Add backlinks to the OP.'
] ]
'Quote Inlining': [ 'Quote Inlining': [
@ -685,8 +689,8 @@ Config =
MD5: '' MD5: ''
sauces: """ sauces: """
http://iqdb.org/?url=%TURL
https://www.google.com/searchbyimage?image_url=%TURL https://www.google.com/searchbyimage?image_url=%TURL
http://iqdb.org/?url=%TURL
#//tineye.com/search?url=%TURL #//tineye.com/search?url=%TURL
#http://saucenao.com/search.php?url=%TURL #http://saucenao.com/search.php?url=%TURL
#http://3d.iqdb.org/?url=%TURL #http://3d.iqdb.org/?url=%TURL
@ -829,6 +833,7 @@ https://www.google.com/searchbyimage?image_url=%TURL
'x' 'x'
'Hide thread.' 'Hide thread.'
] ]
updater: updater:
checkbox: checkbox:
'Beep': [ 'Beep': [

88
src/databoard.coffee Normal file
View File

@ -0,0 +1,88 @@
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts']
class DataBoard
constructor: (@key, sync) ->
@data = Conf[key]
$.sync key, @onSync.bind @
@clean()
return unless sync
# Chrome also fires the onChanged callback on the current tab,
# so we only start syncing when we're ready.
$.on d, '4chanXInitFinished', => @sync = sync
delete: ({boardID, threadID, postID}) ->
if postID
delete @data.boards[boardID][threadID][postID]
@deleteIfEmpty {boardID, threadID}
else if threadID
delete @data.boards[boardID][threadID]
@deleteIfEmpty {boardID}
else
delete @data.boards[boardID]
$.set @key, @data
deleteIfEmpty: ({boardID, threadID}) ->
if threadID
unless Object.keys(@data.boards[boardID][threadID]).length
delete @data.boards[boardID][threadID]
@deleteIfEmpty {boardID}
else unless Object.keys(@data.boards[boardID]).length
delete @data.boards[boardID]
set: ({boardID, threadID, postID, val}) ->
if postID
((@data.boards[boardID] or= {})[threadID] or= {})[postID] = val
else if threadID
(@data.boards[boardID] or= {})[threadID] = val
else
@data.boards[boardID] = val
$.set @key, @data
get: ({boardID, threadID, postID, defaultValue}) ->
if board = @data.boards[boardID]
unless threadID
if postID
for ID, thread in board
if postID of thread
val = thread[postID]
break
else
val = board
else if thread = board[threadID]
val = if postID
thread[postID]
else
thread
val or defaultValue
clean: ->
for boardID of @data.boards
@deleteIfEmpty {boardID}
now = Date.now()
if (@data.lastChecked or 0) < now - 12 * $.HOUR
@data.lastChecked = now
for boardID of @data.boards
@ajaxClean boardID
$.set @key, @data
ajaxClean: (boardID) ->
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
if e.target.status is 404
# Deleted board.
@delete boardID
else if e.target.status is 200
board = @data.boards[boardID]
threads = {}
for page in JSON.parse e.target.response
for thread in page.threads
if thread.no of board
threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads
@deleteIfEmpty {boardID}
$.set @key, @data
onSync: (data) ->
@data = data or boards: {}
@sync?()

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ class Thread
@fullID = "#{@board}.#{@ID}" @fullID = "#{@board}.#{@ID}"
@posts = {} @posts = {}
g.threads["#{board}.#{@}"] = board.threads[@] = @ g.threads[@fullID] = board.threads[@] = @
kill: -> kill: ->
@isDead = true @isDead = true
@ -66,7 +66,11 @@ class Post
@nodes.date = date @nodes.date = date
@info.date = new Date date.dataset.utc * 1000 @info.date = new Date date.dataset.utc * 1000
if Conf['Quick Reply'] if Conf['Quick Reply']
@info.yours = !!QR.yourPosts.threads[@thread.ID]?.contains(@ID) @info.yours = QR.db.get
boardID: @board
threadID: @thread
postID: @ID
@parseComment() @parseComment()
@parseQuotes() @parseQuotes()
@ -109,7 +113,7 @@ class Post
@thread.isClosed = !!$ '.closedIcon', @nodes.info @thread.isClosed = !!$ '.closedIcon', @nodes.info
@clones = [] @clones = []
g.posts["#{board}.#{@}"] = thread.posts[@] = board.posts[@] = @ g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
@kill() if that.isArchived @kill() if that.isArchived
parseComment: -> parseComment: ->
@ -160,10 +164,12 @@ class Post
kill: (file, now) -> kill: (file, now) ->
now or= new Date() now or= new Date()
if file if file
return if @file.isDead
@file.isDead = true @file.isDead = true
@file.timeOfDeath = now @file.timeOfDeath = now
$.addClass @nodes.root, 'deleted-file' $.addClass @nodes.root, 'deleted-file'
else else
return if @isDead
@isDead = true @isDead = true
@timeOfDeath = now @timeOfDeath = now
$.addClass @nodes.root, 'deleted-post' $.addClass @nodes.root, 'deleted-post'
@ -283,7 +289,7 @@ class Clone extends Post
Main = Main =
init: -> init: (items) ->
# flatten Config into Conf # flatten Config into Conf
# and get saved or default values # and get saved or default values
flatten = (parent, obj) -> flatten = (parent, obj) ->
@ -296,8 +302,14 @@ Main =
Conf[parent] = obj Conf[parent] = obj
return return
flatten null, Config flatten null, Config
for key, val of Conf for db in DataBoards
Conf[key] = $.get key, val Conf[db] = boards: {}
$.get Conf, Main.initFeatures
$.on d, '4chanMainInit', Main.initStyle
initFeatures: (items) ->
Conf = items
pathname = location.pathname.split '/' pathname = location.pathname.split '/'
g.BOARD = new Board pathname[1] g.BOARD = new Board pathname[1]
@ -310,7 +322,7 @@ Main =
else else
'index' 'index'
if g.VIEW is 'thread' if g.VIEW is 'thread'
g.THREAD = +pathname[3] g.THREADID = +pathname[3]
# Check if the current board we're on is SFW or not, so we can handle options that need to know that. # Check if the current board we're on is SFW or not, so we can handle options that need to know that.
if ['b', 'd', 'e', 'gif', 'h', 'hc', 'hm', 'hr', 'pol', 'r', 'r9k', 'rs', 's', 'soc', 't', 'u', 'y'].contains g.BOARD if ['b', 'd', 'e', 'gif', 'h', 'hc', 'hm', 'hr', 'pol', 'r', 'r9k', 'rs', 's', 'soc', 't', 'u', 'y'].contains g.BOARD
@ -351,6 +363,7 @@ Main =
return return
# c.time 'All initializations' # c.time 'All initializations'
initFeatures initFeatures
'Polyfill': Polyfill 'Polyfill': Polyfill
'Emoji': Emoji 'Emoji': Emoji
@ -367,14 +380,14 @@ Main =
'Resurrect Quotes': Quotify 'Resurrect Quotes': Quotify
'Filter': Filter 'Filter': Filter
'Thread Hiding': ThreadHiding 'Thread Hiding': ThreadHiding
'Reply Hiding': ReplyHiding 'Reply Hiding': PostHiding
'Recursive': Recursive 'Recursive': Recursive
'Strike-through Quotes': QuoteStrikeThrough 'Strike-through Quotes': QuoteStrikeThrough
'Quick Reply': QR 'Quick Reply': QR
'Menu': Menu 'Menu': Menu
'Report Link': ReportLink 'Report Link': ReportLink
'Thread Hiding (Menu)': ThreadHiding.menu 'Thread Hiding (Menu)': ThreadHiding.menu
'Reply Hiding (Menu)': ReplyHiding.menu 'Reply Hiding (Menu)': PostHiding.menu
'Delete Link': DeleteLink 'Delete Link': DeleteLink
'Filter (Menu)': Filter.menu 'Filter (Menu)': Filter.menu
'Download Link': DownloadLink 'Download Link': DownloadLink
@ -391,6 +404,7 @@ Main =
'File Info Formatting': FileInfo 'File Info Formatting': FileInfo
'Sauce': Sauce 'Sauce': Sauce
'Image Expansion': ImageExpand 'Image Expansion': ImageExpand
'Image Expansion (Menu)': ImageExpand.menu
'Reveal Spoilers': RevealSpoilers 'Reveal Spoilers': RevealSpoilers
'Image Replace': ImageReplace 'Image Replace': ImageReplace
'Image Hover': ImageHover 'Image Hover': ImageHover
@ -404,6 +418,7 @@ Main =
'Thread Watcher': ThreadWatcher 'Thread Watcher': ThreadWatcher
'Index Navigation': Nav 'Index Navigation': Nav
'Keybinds': Keybinds 'Keybinds': Keybinds
# c.timeEnd 'All initializations' # c.timeEnd 'All initializations'
$.on d, 'AddCallback', Main.addCallback $.on d, 'AddCallback', Main.addCallback
@ -413,9 +428,9 @@ Main =
if d.title is '4chan - 404 Not Found' if d.title is '4chan - 404 Not Found'
if Conf['404 Redirect'] and g.VIEW is 'thread' if Conf['404 Redirect'] and g.VIEW is 'thread'
href = Redirect.to href = Redirect.to
board: g.BOARD boardID: g.BOARD.ID
threadID: g.THREAD threadID: g.THREADID
postID: location.hash postID: +location.hash.match /\d+/ # post number or 0
location.href = href or "/#{g.BOARD}/" location.href = href or "/#{g.BOARD}/"
return return
@ -479,30 +494,34 @@ Main =
Klass::callbacks.push obj.callback Klass::callbacks.push obj.callback
checkUpdate: -> checkUpdate: ->
return unless Main.isThisPageLegit() return unless Conf['Check for Updates'] and Main.isThisPageLegit()
# Check for updates after: # Check for updates after:
# - 6 hours since the last update on Opera because it lacks auto-updating. # - 6 hours since the last update on Opera because it lacks auto-updating.
# - 7 days since the last update on Chrome/Firefox. # - 7 days since the last update on Chrome/Firefox.
# After that, check for updates every day if we still haven't updated. # After that, check for updates every day if we still haven't updated.
now = Date.now() now = Date.now()
freq = <% if (type === 'userjs') { %>6 * $.HOUR<% } else { %>7 * $.DAY<% } %> freq = <% if (type === 'userjs') { %>6 * $.HOUR<% } else { %>7 * $.DAY<% } %>
if $.get('lastupdate', 0) > now - freq or $.get('lastchecked', 0) > now - $.DAY items =
return lastupdate: 0
$.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: -> lastchecked: 0
return unless @status is 200 $.get items, (items) ->
version = @response if items.lastupdate > now - freq or items.lastchecked > now - $.DAY
return unless /^\d\.\d+\.\d+$/.test version
if g.VERSION is version
# Don't check for updates too frequently if there wasn't one in a 'long' time.
$.set 'lastupdate', now
return return
$.set 'lastchecked', now $.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: ->
el = $.el 'span', return unless @status is 200
innerHTML: "Update: <%= meta.name %> v#{version} is out, get it <a href=<%= meta.page %> target=_blank>here</a>." version = @response
new Notification 'info', el, 2 * $.MINUTE return unless /^\d\.\d+\.\d+$/.test version
if g.VERSION is version
# Don't check for updates too frequently if there wasn't one in a 'long' time.
$.set 'lastupdate', now
return
$.set 'lastchecked', now
el = $.el 'span',
innerHTML: "Update: <%= meta.name %> v#{version} is out, get it <a href=<%= meta.page %> target=_blank>here</a>."
new Notification 'info', el, 120
handleErrors: (errors) -> handleErrors: (errors) ->
unless 'length' of errors unless errors instanceof Array
error = errors error = errors
else if errors.length is 1 else if errors.length is 1
error = errors[0] error = errors[0]
@ -513,12 +532,10 @@ Main =
div = $.el 'div', div = $.el 'div',
innerHTML: "#{errors.length} errors occurred. [<a href=javascript:;>show</a>]" innerHTML: "#{errors.length} errors occurred. [<a href=javascript:;>show</a>]"
$.on div.lastElementChild, 'click', -> $.on div.lastElementChild, 'click', ->
if @textContent is 'show' [@textContent, logs.hidden] = if @textContent is 'show'
@textContent = 'hide' ['hide', false]
logs.hidden = false
else else
@textContent = 'show' ['show', true]
logs.hidden = true
logs = $.el 'div', logs = $.el 'div',
hidden: true hidden: true
@ -528,15 +545,31 @@ Main =
new Notification 'error', [div, logs], 30 new Notification 'error', [div, logs], 30
parseError: (data) -> parseError: (data) ->
{message, error} = data Main.logError data
c.log message, error
c.log message, error.stack
message = $.el 'div', message = $.el 'div',
textContent: message textContent: data.message
error = $.el 'div', error = $.el 'div',
textContent: error textContent: data.error
[message, error] [message, error]
errors: []
logError: (data) ->
unless Main.errors.length
$.on window, 'unload', Main.postErrors
c.error data.message, data.error.stack
Main.errors.push data
postErrors: ->
errors = Main.errors.map (d) -> d.message + ' ' + d.error.stack
$.ajax '<%= meta.page %>errors', {},
sync: true
form: $.formData
n: "<%= meta.name %> v#{g.VERSION}"
t: '<%= type %>'
ua: window.navigator.userAgent
url: window.location.href
e: errors.join '\n'
isThisPageLegit: -> isThisPageLegit: ->
# 404 error page or similar. # 404 error page or similar.
unless 'thisPageIsLegit' of Main unless 'thisPageIsLegit' of Main

View File

@ -14,7 +14,7 @@
"run_at": "document_start" "run_at": "document_start"
}], }],
"homepage_url": "<%= meta.page %>", "homepage_url": "<%= meta.page %>",
"minimum_chrome_version": "25", "minimum_chrome_version": "26",
"permissions": [ "permissions": [
"storage" "storage"
] ]

View File

@ -1,9 +1,8 @@
QR = QR =
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Quick Reply'] return if !Conf['Quick Reply']
Misc.clearThreads "yourPosts.#{g.BOARD}" @db = new DataBoard 'yourPosts'
@syncYourPosts()
sc = $.el 'a', sc = $.el 'a',
className: "qr-shortcut #{unless Conf['Persistent QR'] then 'disabled' else ''}" className: "qr-shortcut #{unless Conf['Persistent QR'] then 'disabled' else ''}"
@ -93,13 +92,6 @@ QR =
else else
QR.unhide() QR.unhide()
syncYourPosts: (yourPosts) ->
if yourPosts
QR.yourPosts = yourPosts
return
QR.yourPosts = $.get "yourPosts.#{g.BOARD}", threads: {}
$.sync "yourPosts.#{g.BOARD}", QR.syncYourPosts
error: (err) -> error: (err) ->
QR.open() QR.open()
if typeof err is 'string' if typeof err is 'string'
@ -150,10 +142,11 @@ QR =
sage: if board is 'q' then 600 else 60 sage: if board is 'q' then 600 else 60
file: if board is 'q' then 300 else 30 file: if board is 'q' then 300 else 30
post: if board is 'q' then 60 else 30 post: if board is 'q' then 60 else 30
QR.cooldown.cooldowns = $.get "cooldown.#{board}", {}
QR.cooldown.upSpd = 0 QR.cooldown.upSpd = 0
QR.cooldown.upSpdAccuracy = .5 QR.cooldown.upSpdAccuracy = .5
QR.cooldown.start() $.get "cooldown.#{board}", {}, (item) ->
QR.cooldown.cooldowns = item["cooldown.#{board}"]
QR.cooldown.start()
$.sync "cooldown.#{board}", QR.cooldown.sync $.sync "cooldown.#{board}", QR.cooldown.sync
start: -> start: ->
return if QR.cooldown.isCounting return if QR.cooldown.isCounting
@ -356,22 +349,13 @@ QR =
$.addClass QR.nodes.el, 'dump' $.addClass QR.nodes.el, 'dump'
resetThreadSelector: -> resetThreadSelector: ->
if g.VIEW is 'thread' if g.VIEW is 'thread'
QR.nodes.thread.value = g.THREAD QR.nodes.thread.value = g.THREADID
else else
QR.nodes.thread.value = 'new' QR.nodes.thread.value = 'new'
posts: [] posts: []
post: class post: class
constructor: -> constructor: (select) ->
# set values, or null, to avoid 'undefined' values in inputs
prev = QR.posts[QR.posts.length - 1]
persona = $.get 'QR.persona', {}
@name = if prev then prev.name else persona.name or null
@email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null
@sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null
@spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false
@com = null
el = $.el 'a', el = $.el 'a',
className: 'qr-preview' className: 'qr-preview'
draggable: true draggable: true
@ -398,13 +382,32 @@ QR =
for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop'] for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop']
$.on el, event.toLowerCase(), @[event] $.on el, event.toLowerCase(), @[event]
@unlock() prev = QR.posts[QR.posts.length - 1]
QR.posts.push @ QR.posts.push @
@spoiler = if prev and Conf['Remember Spoiler']
prev.spoiler
else
false
$.get 'QR.persona', {}, (item) =>
persona = item['QR.persona']
@name = if prev
prev.name
else
persona.name
@email = if prev and !/^sage$/.test prev.email
prev.email
else
persona.email
if Conf['Remember Subject']
@sub = if prev then prev.sub else persona.sub
@load() if QR.selected is @ # load persona
@select() if select
@unlock()
rm: -> rm: ->
$.rm @nodes.el $.rm @nodes.el
index = QR.posts.indexOf @ index = QR.posts.indexOf @
if QR.posts.length is 1 if QR.posts.length is 1
new QR.post().select() new QR.post true
else if @ is QR.selected else if @ is QR.selected
(QR.posts[index-1] or QR.posts[index+1]).select() (QR.posts[index-1] or QR.posts[index+1]).select()
QR.posts.splice index, 1 QR.posts.splice index, 1
@ -433,9 +436,11 @@ QR =
rectEl = @nodes.el.getBoundingClientRect() rectEl = @nodes.el.getBoundingClientRect()
rectList = @nodes.el.parentNode.getBoundingClientRect() rectList = @nodes.el.parentNode.getBoundingClientRect()
@nodes.el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2 @nodes.el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2
@load()
load: ->
# Load this post's values. # Load this post's values.
for name in ['name', 'email', 'sub', 'com'] for name in ['name', 'email', 'sub', 'com']
QR.nodes[name].value = @[name] QR.nodes[name].value = @[name] or null
@showFileData() @showFileData()
QR.characterCount() QR.characterCount()
save: (input) -> save: (input) ->
@ -586,7 +591,6 @@ QR =
$.on window, 'captcha:timeout', setLifetime $.on window, 'captcha:timeout', setLifetime
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))' $.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
$.off window, 'captcha:timeout', setLifetime $.off window, 'captcha:timeout', setLifetime
c.log @lifetime
imgContainer = $.el 'div', imgContainer = $.el 'div',
className: 'captcha-img' className: 'captcha-img'
@ -611,8 +615,9 @@ QR =
$.on imgContainer, 'click', @reload.bind @ $.on imgContainer, 'click', @reload.bind @
$.on input, 'keydown', @keydown.bind @ $.on input, 'keydown', @keydown.bind @
$.get 'captchas', [], (item) =>
@sync item['captchas']
$.sync 'captchas', @sync $.sync 'captchas', @sync
@sync $.get 'captchas', []
# start with an uncached captcha # start with an uncached captcha
@reload() @reload()
@ -794,13 +799,13 @@ QR =
$.on nodes.autohide, 'change', QR.toggleHide $.on nodes.autohide, 'change', QR.toggleHide
$.on nodes.close, 'click', QR.close $.on nodes.close, 'click', QR.close
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump' $.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
$.on nodes.addPost, 'click', -> new QR.post().select() $.on nodes.addPost, 'click', -> new QR.post true
$.on nodes.form, 'submit', QR.submit $.on nodes.form, 'submit', QR.submit
$.on nodes.fileRM, 'click', -> QR.selected.rmFile() $.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click() $.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
$.on nodes.fileInput, 'change', QR.fileInput $.on nodes.fileInput, 'change', QR.fileInput
new QR.post().select() new QR.post true
# save selected post's data # save selected post's data
for name in ['name', 'email', 'sub', 'com'] for name in ['name', 'email', 'sub', 'com']
$.on nodes[name], 'input', -> QR.selected.save @ $.on nodes[name], 'input', -> QR.selected.save @
@ -839,10 +844,12 @@ QR =
err = 'New threads require a subject.' err = 'New threads require a subject.'
else unless post.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm' else unless post.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
err = 'No file selected.' err = 'No file selected.'
else if g.BOARD.threads[threadID].isSticky else if g.BOARD.threads[threadID].isClosed
err = 'You can\'t reply to this thread anymore.' err = 'You can\'t reply to this thread anymore.'
else unless post.com or post.file else unless post.com or post.file
err = 'No file selected.' err = 'No file selected.'
else if post.file and g.BOARD.threads[threadID].fileLimit
err = 'Max limit of image replies has been reached.'
if QR.captcha.isEnabled and !err if QR.captcha.isEnabled and !err
{challenge, response} = QR.captcha.getOne() {challenge, response} = QR.captcha.getOne()
@ -893,6 +900,7 @@ QR =
QR.error $.el 'span', QR.error $.el 'span',
innerHTML: 'Connection error. You may have been <a href=//www.4chan.org/banned target=_blank>banned</a>.' innerHTML: 'Connection error. You may have been <a href=//www.4chan.org/banned target=_blank>banned</a>.'
opts = opts =
cred: true
form: $.formData postData form: $.formData postData
upCallbacks: upCallbacks:
onload: -> onload: ->
@ -965,21 +973,25 @@ QR =
QR.cleanNotifications() QR.cleanNotifications()
QR.notifications.push new Notification 'success', h1.textContent, 5 QR.notifications.push new Notification 'success', h1.textContent, 5
persona = $.get 'QR.persona', {} $.get 'QR.persona', {}, (item) ->
persona = persona = item['QR.persona']
name: post.name persona =
email: if /^sage$/.test post.email then persona.email else post.email name: post.name
sub: if Conf['Remember Subject'] then post.sub else null email: if /^sage$/.test post.email then persona.email else post.email
$.set 'QR.persona', persona sub: if Conf['Remember Subject'] then post.sub else null
$.set 'QR.persona', persona
[_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/ [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
postID = +postID postID = +postID
threadID = +threadID or postID threadID = +threadID or postID
isReply = threadID isnt postID isReply = threadID isnt postID
(QR.yourPosts.threads[threadID] or= []).push postID QR.db.set
$.set "yourPosts.#{g.BOARD}", QR.yourPosts boardID: g.BOARD.ID
threadID: threadID
postID: postID
val: true
ThreadUpdater.postID = postID ThreadUpdater.postID = postID
# Post/upload confirmed as successful. # Post/upload confirmed as successful.
@ -992,7 +1004,10 @@ QR =
# Enable auto-posting if we have stuff to post, disable it otherwise. # Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.posts.length > 1 and isReply QR.cooldown.auto = QR.posts.length > 1 and isReply
post.rm() unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close()
else
post.rm()
QR.cooldown.set {req, post, isReply} QR.cooldown.set {req, post, isReply}
@ -1006,9 +1021,6 @@ QR =
else else
window.location = "/#{g.BOARD}/res/#{threadID}" window.location = "/#{g.BOARD}/res/#{threadID}"
unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close()
QR.status() QR.status()
abort: -> abort: ->