diff --git a/CHANGELOG.md b/CHANGELOG.md
index 407809490..a3e8248f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,89 @@
-Sometimes the changelog has notes (not comprehensive) acknowledging people's work. This does not mean the changes are their fault, only that their code was used. All changes to the script are chosen by and the fault of the maintainer (ccd0).
+### v1.14.7
+
+**v1.14.7.2** *(2019-04-11)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.2/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.2/builds/4chan-X-noupdate.crx)]
+- Fix dragging left to contract WebMs in Firefox. #1547
+- Remove query string from filename in Post from URL feature.
+- Speed up Post from URL on some platforms.
+- Fix issue making WebM title fetching needlessly slow on Chrome extension.
+
+**v1.14.7.1** *(2019-04-09)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.1/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.1/builds/4chan-X-noupdate.crx)]
+- Tolerate broken HTML better.
+- Fix 4chan/4channel not being correct in certain links.
+- Use boards.json to determine whether to activate [code] and [math] tag related functions. #525
+
+**v1.14.7.0** *(2019-04-07)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.0/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.7.0/builds/4chan-X-noupdate.crx)]
+- Based on v1.14.6.8.
+- (Teasday) Hotkey to toggle quote threading, `Shift+t` by default.
+- Show what pages watched threads are on. Can be disabled by unchecking `Show Page` in the thread watcher menu. #1030
+- Move Thread Watcher settings out of submenu.
+- Restore filtering on the email field. #2171
+- Support specifying the sites that filters apply to. #2171
+- Make per-board filtering work on boards with unusual characters in the name (e.g. certain lainchan boards).
+- Board names in filters are now case-sensitive.
+
+### v1.14.6
+
+**v1.14.6.8** *(2019-04-06)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.8/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.8/builds/4chan-X-noupdate.crx)]
+- Update list of boards on https://catalog.neet.tv/.
+
+**v1.14.6.7** *(2019-04-05)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.7/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.7/builds/4chan-X-noupdate.crx)]
+- Update .crx files to CRX3. This should fix the errors when attempting to install them on newer versions of Chromium.
+
+**v1.14.6.6** *(2019-04-05)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.6/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.6/builds/4chan-X-noupdate.crx)]
+- Sauce: Update DeviantArt filename format. #2237
+- Sauce: Replace unmatched regex groups with empty string, not 'undefined'
+- Whether to add parameter to avoid cache should be based on site being queried, not site currenly on.
+
+**v1.14.6.5** *(2019-04-04)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.5/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.5/builds/4chan-X-noupdate.crx)]
+- Fix Thread Watcher bug that in certain circumstances caused the last check of an archived thread for new replies to be skipped.
+
+**v1.14.6.4** *(2019-04-02)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.4/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.4/builds/4chan-X-noupdate.crx)]
+- Merge v1.14.5.16: Remove score/perks message. Fix Posting Success Notifications.
+- Merge v1.14.5.16: Remove like buttons. Continue to show like counts and scores where given in API.
+- Bugfix: Account for posts added by thread expansion when marking read from index.
+
+**v1.14.6.3** *(2019-04-01)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.3/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.3/builds/4chan-X-noupdate.crx)]
+- Merge v1.14.5.15: Show info relating to April 2019 event. #2266
+
+**v1.14.6.2** *(2019-03-31)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.2/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.2/builds/4chan-X-noupdate.crx)]
+- Support filters that apply to multiple post fields joined by newline characters.
+
+**v1.14.6.1** *(2019-03-30)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.1/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.1/builds/4chan-X-noupdate.crx)]
+- Fix errors in certain userscript managers introduced in v1.14.6.0. #2256
+
+**v1.14.6.0** *(2019-03-25)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.0/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.6.0/builds/4chan-X-noupdate.crx)]
+- Based on v1.14.5.14.
+- (ebinBuddha) Added desktop notification for filters (`notify` option).
+- Make it possible to filter posts without ID (use `//`). #1578
+- Add `file` option to filter only posts with/without files.
+- Improvements in Thread Watcher efficiency, particularly when using it with multiple sites.
+- Allow image hover previews to use full width of screen even in cases where it covers the thumbnail.
+- Make movement of image hover / quote preview with mouse optional; option is `Follow Cursor`. #471, #2245
+- Fix image/video hover in case where dimensions are not available. #2197
+- Implement pruning of data for dead threads on vichan sites with JSON API. #2171
+- Override 4chan CSS causing sauce links to get cut off. #2193
+- Change export URL from data: to blob: so larger settings files can be exported. #2255
+- Unbreak warning in Chrome extension to reload the page after an update.
+- Various minor bugfixes.
+
### v1.14.5
+**v1.14.5.16** *(2019-04-02)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.16/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.16/builds/4chan-X-noupdate.crx)]
+- Remove score/perks message. Fix Posting Success Notifications.
+- Remove like buttons. Continue to show like counts and scores where given in API.
+
+**v1.14.5.15** *(2019-04-01)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.15/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.15/builds/4chan-X-noupdate.crx)]
+- Show info relating to April 2019 event. #2266
+- Override 4chan CSS causing sauce links to get cut off. #2193
+- Unbreak warning in Chrome extension to reload the page after an update.
+
+**v1.14.5.14** *(2019-03-22)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.14/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.14/builds/4chan-X-noupdate.crx)]
+- Add message alerting Chrome extension users to disable chrome://flags/#network-service
+- Minor bugfix in catalog/index loading.
+
**v1.14.5.13** *(2019-03-08)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.13/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.13/builds/4chan-X-noupdate.crx)]
- Fix bugs related to additional permissions requests. #2230
- Revert changes in thread watcher that caused performance decrease.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 92fcdefe7..abdb00ff3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,7 +30,8 @@ If you're reporting a bug, the more detail you can give, the better. If I can't
- 4chan X is mostly written in [CoffeeScript](http://coffeescript.org/). If you're already familiar with Javascript, it doesn't take long to pick up.
- Edit the sources in the src/ directory (not the compiled scripts in builds/).
-- Compile the script with: `make` (this should fetch needed dependencies automatically; if not, do an `npm install` first)
+- Fetch needed dependencies with: `npm install`
+- Compile the script with: `make`
- Install the compiled script (found in the testbuilds/ directory), and test your changes.
- Make sure you have set your name and email as you want them, as they will be published in your commit message: `git config user.name yourname` `git config user.email youremail`
- Commit your changes: `git commit -a`
diff --git a/Makefile b/Makefile
index f88841cff..7d5ca2ab8 100644
--- a/Makefile
+++ b/Makefile
@@ -15,17 +15,16 @@ else
endif
CP = $(call CAT,$<,$@)
-npgoals := clean cleanrel cleanweb cleanfull withtests wrapped archives $(foreach i,1 2 3 4,bump$(i)) tag tagcommit beta stable web update updatehard
+npgoals := clean cleanrel cleanweb cleanfull withtests archives $(foreach i,1 2 3 4,bump$(i)) tag tagcommit beta stable web update updatehard
ifneq "$(filter $(npgoals),$(MAKECMDGOALS))" ""
.NOTPARALLEL :
endif
coffee := $(BIN)coffee -c --no-header
-coffee_deps := node_modules/coffee-script/package.json
template := node tools/template.js
-template_deps := package.json tools/template.js node_modules/lodash.template/package.json node_modules/esprima/package.json
+template_deps := package.json tools/template.js
-# read name meta_name meta_distBranch meta_uploadPath
+# read name meta_name meta_distBranch
$(eval $(shell node tools/pkgvars.js))
# must be read in when needed to prevent out-of-date version
@@ -55,7 +54,7 @@ uses_tests_enabled := \
imports_src/globals/globals.js := \
version.json
imports_src/css/CSS.js := \
- node_modules/font-awesome/package.json
+ node_modules/font-awesome/fonts/fontawesome-webfont.woff
imports_src/Monitoring/Favicon.coffee := \
src/meta/icon128.png
@@ -104,22 +103,6 @@ all : default release
.events .events2 tmp testbuilds builds :
$(MKDIR)
-ifneq "$(wildcard npm-shrinkwrap.json)" ""
-
-.events/npm : npm-shrinkwrap.json | .events
- npm install
- echo -> $@
-
-node_modules/%/package.json : .events/npm
- $(if $(wildcard $@),,npm install && echo -> $<)
-
-else
-
-node_modules/%/package.json : package.json
- npm install $(call QUOTE,$*@$(version_$*))
-
-endif
-
.tests_enabled :
echo false> .tests_enabled
@@ -137,7 +120,7 @@ endef
$(foreach s,$(sources),$(eval $(call check_source,$(subst $$,$$$$,$(s)))))
-.events/compile : $(updates) $(template_deps) $(coffee_deps) tools/chain.js
+.events/compile : $(updates) $(template_deps) tools/chain.js
node tools/chain.js $(call QUOTE, \
$(subst .events/,tmp/, \
$(if $(filter-out $(updates),$?), \
@@ -154,7 +137,7 @@ $(dests) : .events/compile
&& echo -> $< \
)
-tmp/eventPage.js : src/meta/eventPage.coffee $(coffee_deps) | tmp
+tmp/eventPage.js : src/meta/eventPage.coffee | tmp
$(coffee) -o tmp src/meta/eventPage.coffee
tmp/LICENSE : LICENSE tools/newlinefix.js | tmp
@@ -189,11 +172,11 @@ testbuilds/updates$1.json : src/meta/updates.json version.json $(template_deps)
testbuilds/$(name)$1.crx.zip : \
$(foreach f,$(crx_contents),testbuilds/crx$1/$(f)) \
- package.json version.json tools/zip-crx.js node_modules/jszip/package.json
+ package.json version.json tools/zip-crx.js
node tools/zip-crx.js $1
-testbuilds/$(name)$1.crx : testbuilds/$(name)$1.crx.zip package.json tools/sign.js node_modules/node-rsa/package.json
- node tools/sign.js $1
+testbuilds/$(name)$1.crx : $(foreach f,$(crx_contents),testbuilds/crx$1/$(f)) version.json tools/sign.sh | tmp
+ tools/sign.sh $1
testbuilds/$(name)$1.meta.js : src/meta/metadata.js src/meta/icon48.png version.json src/Archive/archives.json $(template_deps) | testbuilds
$(template) $$< $$@ type=userscript channel=$1
@@ -214,7 +197,7 @@ testbuilds/$(name).zip : testbuilds/$(name)-noupdate.crx.zip
builds/% : testbuilds/% | builds
$(CP)
-test.html : README.md template.jst tools/markdown.js node_modules/markdown-it/package.json node_modules/markdown-it-anchor/package.json node_modules/lodash.template/package.json
+test.html : README.md template.jst tools/markdown.js
node tools/markdown.js
index.html : test.html
@@ -223,7 +206,7 @@ index.html : test.html
tmp/.jshintrc : src/meta/jshint.json tmp/declaration.js src/globals/globals.js $(template_deps) | tmp
$(template) $< $@
-.events/jshint : $(dests) tmp/.jshintrc node_modules/jshint/package.json
+.events/jshint : $(dests) tmp/.jshintrc
$(BIN)jshint $(call QUOTE, \
$(if $(filter-out $(dests),$?), \
$(dests), \
@@ -263,13 +246,13 @@ distready : dist $(wildcard dist/* dist/*/*)
git push web $(meta_distBranch)
echo -> $@
-.events2/push-store : .git/refs/tags/stable | .events2 distready node_modules/webstore-upload/package.json node_modules/request/package.json
+.events2/push-store : .git/refs/tags/stable | .events2 distready
node tools/webstore.js
echo -> $@
.SECONDARY :
-.PHONY: default all distready script crx release jshint install push captchas $(npgoals)
+.PHONY: default all distready script crx release jshint install push $(npgoals)
script : $(script)
@@ -283,23 +266,18 @@ install : .events/install
push : .events2/push-git .events2/push-web .events2/push-store
-captchas : redirect.html $(template_deps)
- $(template) redirect.html captchas.html url="$(url)"
- scp captchas.html $(meta_uploadPath)
-
clean :
- $(RMDIR) tmp testbuilds .events
+ $(RMDIR) tmp tmp-crx testbuilds .events
$(RM) .tests_enabled
cleanrel : clean
$(RMDIR) builds
cleanweb :
- $(RM) test.html captchas.html
+ $(RM) test.html
cleanfull : clean cleanweb
$(RMDIR) .events2 dist node_modules
- $(RM) npm-shrinkwrap.json
git worktree prune
withtests :
@@ -307,10 +285,6 @@ withtests :
-$(MAKE)
echo false> .tests_enabled
-wrapped : src/meta/npm-shrinkwrap.json
- $(call CAT,$<,npm-shrinkwrap.json)
- npm install
-
archives :
git fetch -n archives
git merge --no-commit -s ours archives/gh-pages
@@ -326,7 +300,6 @@ $(foreach i,1 2 3 4,bump$(i)) :
tag :
git add builds
$(MAKE) cleanrel
- $(MAKE) wrapped
$(MAKE) all
git diff --quiet -- builds
$(MAKE) tagcommit
@@ -355,15 +328,11 @@ web : index.html distready
cd dist && git commit -am "Update web page."
update :
- $(RM) npm-shrinkwrap.json
+ $(RM) package-lock.json
npm install --save-dev $(shell node tools/unpinned.js)
npm install
- npm shrinkwrap --dev
- $(call CAT,npm-shrinkwrap.json,src/meta/npm-shrinkwrap.json)
updatehard :
- $(RM) npm-shrinkwrap.json
+ $(RM) package-lock.json
npm install --save-dev $(shell node tools/unpinned.js latest)
npm install
- npm shrinkwrap --dev
- $(call CAT,npm-shrinkwrap.json,src/meta/npm-shrinkwrap.json)
diff --git a/README.md b/README.md
index bb28b0406..c74d959ff 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ https://github.com/KevinParnell/OneeChan.
## Install
### Firefox
-Install [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/), [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
+Install [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/), [Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), or [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) (issues since v4: [#2526](https://github.com/greasemonkey/greasemonkey/issues/2526), [#2576](https://github.com/greasemonkey/greasemonkey/issues/2574)), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
Ports of Greasemonkey are available for [SeaMonkey](https://sourceforge.net/projects/gmport/) and [Pale Moon](https://github.com/janekptacijarabaci/greasemonkey/releases/latest).
diff --git a/builds/4chan-X-beta.crx b/builds/4chan-X-beta.crx
index 55390a6e8..165cdf911 100644
Binary files a/builds/4chan-X-beta.crx and b/builds/4chan-X-beta.crx differ
diff --git a/builds/4chan-X-beta.meta.js b/builds/4chan-X-beta.meta.js
index 8ff33536f..820447f6e 100644
--- a/builds/4chan-X-beta.meta.js
+++ b/builds/4chan-X-beta.meta.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X beta
-// @version 1.14.5.13
+// @version 1.14.7.2
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
diff --git a/builds/4chan-X-beta.user.js b/builds/4chan-X-beta.user.js
index 79201e1a2..a9d40bbb6 100644
--- a/builds/4chan-X-beta.user.js
+++ b/builds/4chan-X-beta.user.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X beta
-// @version 1.14.5.13
+// @version 1.14.7.2
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@@ -198,7 +198,7 @@ docSet = function() {
};
g = {
- VERSION: '1.14.5.13',
+ VERSION: '1.14.7.2',
NAMESPACE: '4chan X.',
boards: {}
};
@@ -241,6 +241,7 @@ Config = (function() {
'JSON Index': [true, 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'],
'Use 4chan X Catalog': [true, 'Link to 4chan X\'s catalog instead of the native 4chan one.', 1],
'Index Refresh Notifications': [false, 'Show a notice at the top of the page when the index is refreshed.', 1],
+ 'Follow Cursor': [true, 'Image Hover and Quote Preview move with the mouse cursor.'],
'Open Threads in New Tab': [false, 'Make links to threads in the index / 4chan X catalog open in a new tab.'],
'External Catalog': [false, 'Link to external catalog instead of the internal one.'],
'Catalog Links': [false, 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'],
@@ -410,6 +411,7 @@ Config = (function() {
'Auto Watch': [true, 'Automatically watch threads you start.'],
'Auto Watch Reply': [true, 'Automatically watch threads you reply to.'],
'Auto Prune': [false, 'Automatically remove dead threads.'],
+ 'Show Page': [true, 'Show what page watched threads are on.'],
'Show Unread Count': [true, 'Show number of unread posts in watched threads.'],
'Show Site Prefix': [true, 'When multiple sites are shown in the thread watcher, add a prefix to board names to distinguish them.'],
'Require OP Quote Link': [false, 'For purposes of thread watcher highlighting, only consider posts with a quote link to the OP as replies to the OP.']
@@ -422,6 +424,7 @@ Config = (function() {
tripcode: "# Filter any tripfag\n#/^!/",
capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for admins:\n#/Admin$/;highlight:admin;op:yes",
pass: "# Filter anyone using since4pass:\n#/./",
+ email: '',
subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only",
comment: "# Filter Stallman copypasta on /g/:\n#/what you\'re refer+ing to as linux/i;boards:g\n# Filter posts with 20 or more quote links:\n#/(?:>>\\d(?:(?!>>\\d)[^])*){20}/\n# Filter posts like T H I S / H / I / S:\n#/^>?\\s?\\w\\s?(\\w)\\s?(\\w)\\s?(\\w).*$[\\s>]+\\1[\\s>]+\\2[\\s>]+\\3/im",
flag: '',
@@ -430,7 +433,7 @@ Config = (function() {
filesize: '',
MD5: ''
},
- sauces: "# Known filename formats:\nhttp://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/\n//%$1.deviantart.com/gallery/#/d%$2;regexp:/^\\w+_by_(\\w+)-d([\\da-z]+)/\n//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/\nhttp://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/\nhttps://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/\n\n# Reverse image search:\nhttps://www.google.com/searchbyimage?image_url=%IMG&safe=off\nhttps://www.yandex.com/images/search?rpt=imageview&img_url=%IMG\n#//tineye.com/search?url=%IMG\n#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights\n\n# Specialized reverse image search:\n//iqdb.org/?url=%IMG\nhttps://trace.moe/?auto&url=%IMG;text:wait\n#//3d.iqdb.org/?url=%IMG\n#//saucenao.com/search.php?url=%IMG\n\n# \"View Same\" in archives:\nhttp://eye.swfchan.com/search/?q=%name;types:swf\n#https://desuarchive.org/_/search/image/%sMD5/\n#https://archive.4plebs.org/_/search/image/%sMD5/\n#https://boards.fireden.net/_/search/image/%sMD5/\n#https://foolz.fireden.net/_/search/image/%sMD5/\n\n# Other tools:\n#http://exif.regex.info/exif.cgi?imgurl=%URL\n#//imgops.com/%URL;types:gif,jpg,png\n#//www.gif-explode.com/%URL;types:gif",
+ sauces: "# Known filename formats:\nhttp://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/\n//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/\n//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/\nhttp://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/\nhttps://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/\n\n# Reverse image search:\nhttps://www.google.com/searchbyimage?image_url=%IMG&safe=off\nhttps://www.yandex.com/images/search?rpt=imageview&img_url=%IMG\n#//tineye.com/search?url=%IMG\n#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights\n\n# Specialized reverse image search:\n//iqdb.org/?url=%IMG\nhttps://trace.moe/?auto&url=%IMG;text:wait\n#//3d.iqdb.org/?url=%IMG\n#//saucenao.com/search.php?url=%IMG\n\n# \"View Same\" in archives:\nhttp://eye.swfchan.com/search/?q=%name;types:swf\n#https://desuarchive.org/_/search/image/%sMD5/\n#https://archive.4plebs.org/_/search/image/%sMD5/\n#https://boards.fireden.net/_/search/image/%sMD5/\n#https://foolz.fireden.net/_/search/image/%sMD5/\n\n# Other tools:\n#http://exif.regex.info/exif.cgi?imgurl=%URL\n#//imgops.com/%URL;types:gif,jpg,png\n#//www.gif-explode.com/%URL;types:gif",
FappeT: {
werk: false
},
@@ -497,6 +500,7 @@ Config = (function() {
'Update': ['r', 'Update the thread / refresh the index.'],
'Update thread watcher': ['Shift+r', 'Manually refresh thread watcher.'],
'Toggle thread watcher': ['t', 'Toggle visibility of thread watcher.'],
+ 'Toggle threading': ['Shift+t', 'Toggle threading.'],
'Mark thread read': ['Ctrl+0', 'Mark thread read from index (requires "Unread Line in Index").'],
'Expand image': ['Shift+e', 'Expand selected image.'],
'Expand images': ['e', 'Expand all images.'],
@@ -1390,6 +1394,10 @@ body.is_catalog .thread > a > img {\n\
.nwsb {\n\
display: inline;\n\
}\n\
+.fileText {\n\
+ max-width: auto;\n\
+ white-space: normal;\n\
+}\n\
/* Ads */\n\
.ad-cnt > *, .adg-rects > *, .bsa-cnt {\n\
height: auto !important;\n\
@@ -2412,12 +2420,11 @@ span.hide-announcement {\n\
-webkit-flex-direction: row;\n\
flex-direction: row;\n\
}\n\
+#watched-threads .watcher-page,\n\
#watched-threads .watcher-unread {\n\
-webkit-flex: 0 0 auto;\n\
flex: 0 0 auto;\n\
-}\n\
-#watched-threads .watcher-unread::after {\n\
- content: \"\\00a0\";\n\
+ margin-right: 2px;\n\
}\n\
#watched-threads .watcher-title {\n\
overflow: hidden;\n\
@@ -2425,7 +2432,10 @@ span.hide-announcement {\n\
-webkit-flex: 0 1 auto;\n\
flex: 0 1 auto;\n\
}\n\
-.replies-quoting-you > a, #watcher-link.replies-quoting-you {\n\
+#watched-threads .watcher-title:not(:first-child) {\n\
+ margin-left: 2px;\n\
+}\n\
+.replies-quoting-you > a, #watcher-link.replies-quoting-you, .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
#thread-watcher a {\n\
@@ -2587,6 +2597,13 @@ span.hide-announcement {\n\
.fileThumb > .warning {\n\
clear: both;\n\
}\n\
+#ihover {\n\
+ pointer-events: none;\n\
+ /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */\n\
+ max-height: 95vh;\n\
+ max-height: calc(100vh - 25px);\n\
+ max-width: 100vw;\n\
+}\n\
/* WEBM Metadata */\n\
.webm-title > a::before {\n\
content: \"title\";\n\
@@ -3699,7 +3716,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(240,224,214,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you {\n\
+:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you, :root.yotsuba .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -3881,7 +3898,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(240,224,214,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you {\n\
+:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you, :root.futaba .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -3974,7 +3991,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(214,218,240,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you {\n\
+:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you, :root.burichan .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4135,7 +4152,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(40,42,46,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you {\n\
+:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you, :root.tomorrow .last-page > a > .watcher-page {\n\
color: #F00 !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4227,7 +4244,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(221,221,221,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you {\n\
+:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you, :root.photon .last-page > a > .watcher-page {\n\
color: #00F !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4382,7 +4399,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(23,21,38,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you {\n\
+:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you, :root.spooky .last-page > a > .watcher-page {\n\
color: #F00 !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4595,58 +4612,43 @@ $ = (function() {
};
$.ajax = (function() {
- var lastModified, pageXHR;
- lastModified = {};
+ var pageXHR;
if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) {
pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest);
} else {
pageXHR = XMLHttpRequest;
}
- return function(url, options, extra) {
- var bypassCache, err, event, form, j, len, params, r, ref, ref1, type, upCallbacks, url0, whenModified;
+ return function(url, options) {
+ var err, form, headers, key, onloadend, onprogress, r, ref, responseType, timeout, type, value, withCredentials;
if (options == null) {
options = {};
}
- if (extra == null) {
- extra = {};
- }
- type = extra.type, whenModified = extra.whenModified, bypassCache = extra.bypassCache, upCallbacks = extra.upCallbacks, form = extra.form;
- if (/\.json$/.test(url)) {
- if (options.responseType == null) {
- options.responseType = 'json';
- }
+ onloadend = options.onloadend, timeout = options.timeout, responseType = options.responseType, withCredentials = options.withCredentials, type = options.type, onprogress = options.onprogress, form = options.form, headers = options.headers;
+ if (responseType == null) {
+ responseType = 'json';
}
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/');
- if (whenModified) {
- params = [];
- if ($.engine === 'blink') {
- params.push("s=" + whenModified);
- }
- if (Site.software === 'yotsuba' && bypassCache) {
- params.push("t=" + (Date.now()));
- }
- url0 = url;
- if (params.length) {
- url += '?' + params.join('&');
- }
- }
r = new pageXHR();
type || (type = form && 'post' || 'get');
try {
r.open(type, url, true);
- if (whenModified) {
- if (((ref = lastModified[whenModified]) != null ? ref[url0] : void 0) != null) {
- r.setRequestHeader('If-Modified-Since', lastModified[whenModified][url0]);
- }
- $.on(r, 'load', function() {
- return (lastModified[whenModified] || (lastModified[whenModified] = {}))[url0] = r.getResponseHeader('Last-Modified');
- });
+ ref = headers || {};
+ for (key in ref) {
+ value = ref[key];
+ r.setRequestHeader(key, value);
}
- $.extend(r, options);
- $.extend(r.upload, upCallbacks);
+ $.extend(r, {
+ onloadend: onloadend,
+ timeout: timeout,
+ responseType: responseType,
+ withCredentials: withCredentials
+ });
+ $.extend(r.upload, {
+ onprogress: onprogress
+ });
$.on(r, 'error', function() {
if (!r.status) {
- return c.error("4chan X failed to load: " + url);
+ return c.warn("4chan X failed to load: " + url);
}
});
r.send(form);
@@ -4655,51 +4657,82 @@ $ = (function() {
if (err.result !== 0x805e0006) {
throw err;
}
- ref1 = ['error', 'loadend'];
- for (j = 0, len = ref1.length; j < len; j++) {
- event = ref1[j];
- r["on" + event] = options["on" + event];
- $.queueTask($.event, event, null, r);
- }
+ r.onloadend = onloadend;
+ $.queueTask($.event, 'error', null, r);
+ $.queueTask($.event, 'loadend', null, r);
}
return r;
};
})();
+ $.lastModified = {};
+
+ $.whenModified = function(url, bucket, cb, options) {
+ var ajax, headers, params, r, ref, t, timeout, url0;
+ if (options == null) {
+ options = {};
+ }
+ timeout = options.timeout, ajax = options.ajax;
+ params = [];
+ if ($.engine === 'blink') {
+ params.push("s=" + bucket);
+ }
+ if (url.split('/')[2] === 'a.4cdn.org') {
+ params.push("t=" + (Date.now()));
+ }
+ url0 = url;
+ if (params.length) {
+ url += '?' + params.join('&');
+ }
+ headers = {};
+ if ((t = (ref = $.lastModified[bucket]) != null ? ref[url0] : void 0) != null) {
+ headers['If-Modified-Since'] = t;
+ }
+ r = (ajax || $.ajax)(url, {
+ onloadend: function() {
+ var base;
+ ((base = $.lastModified)[bucket] || (base[bucket] = {}))[url0] = this.getResponseHeader('Last-Modified');
+ return cb.call(this);
+ },
+ timeout: timeout,
+ headers: headers
+ });
+ return r;
+ };
+
(function() {
var reqs;
reqs = {};
$.cache = function(url, cb, options) {
- var err, req, rm;
- if (req = reqs[url]) {
- if (req.readyState === 4) {
- $.queueTask(function() {
- return cb.call(req, req.evt, true);
- });
- } else {
+ var ajax, onloadend, req;
+ if (options == null) {
+ options = {};
+ }
+ ajax = options.ajax;
+ if ((req = reqs[url])) {
+ if (req.callbacks) {
req.callbacks.push(cb);
+ } else {
+ $.queueTask(function() {
+ return cb.call(req, {
+ isCached: true
+ });
+ });
}
return req;
}
- rm = function() {
- return delete reqs[url];
- };
- try {
- if (!(req = $.ajax(url, options))) {
- return;
- }
- } catch (_error) {
- err = _error;
- return;
- }
- $.on(req, 'load', function(e) {
+ onloadend = function() {
var fn1, j, len, ref;
- this.evt = e;
+ if (!this.status) {
+ delete reqs[url];
+ }
ref = this.callbacks;
fn1 = (function(_this) {
return function(cb) {
return $.queueTask(function() {
- return cb.call(_this, e, false);
+ return cb.call(_this, {
+ isCached: false
+ });
});
};
})(this);
@@ -4708,8 +4741,10 @@ $ = (function() {
fn1(cb);
}
return delete this.callbacks;
+ };
+ req = (ajax || $.ajax)(url, {
+ onloadend: onloadend
});
- $.on(req, 'abort error', rm);
req.callbacks = [cb];
return reqs[url] = req;
};
@@ -5402,25 +5437,25 @@ $$ = (function() {
}).call(this);
CrossOrigin = (function() {
- var CrossOrigin;
+ var CrossOrigin, Request;
CrossOrigin = {
binary: function(url, cb, headers) {
- var options, ref, workaround;
if (headers == null) {
headers = {};
}
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/');
- workaround = $.engine === 'gecko' && (typeof GM_info !== "undefined" && GM_info !== null) && /^[0-2]\.|^3\.[01](?!\d)/.test(GM_info.version);
- workaround || (workaround = /PaleMoon\//.test(navigator.userAgent));
- workaround || (workaround = (typeof GM_info !== "undefined" && GM_info !== null ? (ref = GM_info.script) != null ? ref.includeJSB : void 0 : void 0) != null);
- options = {
+ return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
method: "GET",
url: url,
headers: headers,
+ responseType: 'arraybuffer',
+ overrideMimeType: 'text/plain; charset=x-user-defined',
onload: function(xhr) {
- var contentDisposition, contentType, data, i, r, ref1, ref2;
- if (workaround) {
+ var data, i, r;
+ if (xhr.response instanceof ArrayBuffer) {
+ data = new Uint8Array(xhr.response);
+ } else {
r = xhr.responseText;
data = new Uint8Array(r.length);
i = 0;
@@ -5428,12 +5463,8 @@ CrossOrigin = (function() {
data[i] = r.charCodeAt(i);
i++;
}
- } else {
- data = new Uint8Array(xhr.response);
}
- contentType = (ref1 = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)) != null ? ref1[1] : void 0;
- contentDisposition = (ref2 = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)) != null ? ref2[1] : void 0;
- return cb(data, contentType, contentDisposition);
+ return cb(data, xhr.responseHeaders);
},
onerror: function() {
return cb(null);
@@ -5441,27 +5472,23 @@ CrossOrigin = (function() {
onabort: function() {
return cb(null);
}
- };
- if (workaround) {
- options.overrideMimeType = 'text/plain; charset=x-user-defined';
- } else {
- options.responseType = 'arraybuffer';
- }
- return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)(options);
+ });
},
file: function(url, cb) {
- return CrossOrigin.binary(url, function(data, contentType, contentDisposition) {
- var blob, match, mime, name, ref, ref1, ref2, ref3;
+ return CrossOrigin.binary(url, function(data, headers) {
+ var blob, contentDisposition, contentType, match, mime, name, ref, ref1, ref2, ref3, ref4;
if (data == null) {
return cb(null);
}
- name = (ref = url.match(/([^\/]+)\/*$/)) != null ? ref[1] : void 0;
+ name = (ref = url.match(/([^\/?#]+)\/*(?:$|[?#])/)) != null ? ref[1] : void 0;
+ contentType = (ref1 = headers.match(/Content-Type:\s*(.*)/i)) != null ? ref1[1] : void 0;
+ contentDisposition = (ref2 = headers.match(/Content-Disposition:\s*(.*)/i)) != null ? ref2[1] : void 0;
mime = (contentType != null ? contentType.match(/[^;]*/)[0] : void 0) || 'application/octet-stream';
- match = (contentDisposition != null ? (ref1 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref1[1] : void 0 : void 0) || (contentType != null ? (ref2 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref2[1] : void 0 : void 0);
+ match = (contentDisposition != null ? (ref3 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref3[1] : void 0 : void 0) || (contentType != null ? (ref4 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref4[1] : void 0 : void 0);
if (match) {
name = match.replace(/\\"/g, '"');
}
- if ((typeof GM_info !== "undefined" && GM_info !== null ? (ref3 = GM_info.script) != null ? ref3.includeJSB : void 0 : void 0) != null) {
+ if (/^text\/plain;\s*charset=x-user-defined$/i.test(mime)) {
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] || 'application/octet-stream';
}
blob = new Blob([data], {
@@ -5471,95 +5498,94 @@ CrossOrigin = (function() {
return cb(blob);
});
},
- json: (function() {
- var callbacks, failure, results, success;
- callbacks = {};
- results = {};
- success = function(url, result) {
- var cb, j, len, ref;
- ref = callbacks[url];
- for (j = 0, len = ref.length; j < len; j++) {
- cb = ref[j];
- $.queueTask(function() {
- return cb.call(result);
- });
- }
- delete callbacks[url];
- return results[url] = result;
- };
- failure = function(url) {
- var cb, j, len, ref;
- ref = callbacks[url];
- for (j = 0, len = ref.length; j < len; j++) {
- cb = ref[j];
- $.queueTask(function() {
- return cb.call({});
- });
- }
- return delete callbacks[url];
- };
- return function(url, cb, bypassCache, timeout) {
- var req;
- if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) {
- if (bypassCache) {
- $.cleanCache(function(url2) {
- return url2 === url;
- });
- }
- if ((req = $.cache(url, cb, {
- responseType: 'json'
- }))) {
- $.on(req, 'abort error', function() {
- return cb.call({});
- });
- } else {
- cb.call({});
- }
- return;
- }
- if (bypassCache) {
- delete results[url];
- } else {
- if (results[url]) {
- cb.call(results[url]);
- return;
- }
- if (callbacks[url]) {
- callbacks[url].push(cb);
- return;
- }
- }
- callbacks[url] = [cb];
- return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
- method: "GET",
- url: url + '',
- timeout: timeout,
- onload: function(xhr) {
- var response, status, statusText;
- status = xhr.status, statusText = xhr.statusText;
- try {
- response = JSON.parse(xhr.responseText);
- return success(url, {
- status: status,
- statusText: statusText,
- response: response
- });
- } catch (_error) {
- return failure(url);
+ Request: Request = (function() {
+ function Request() {}
+
+ Request.prototype.status = 0;
+
+ Request.prototype.statusText = '';
+
+ Request.prototype.response = null;
+
+ Request.prototype.responseHeaderString = null;
+
+ Request.prototype.getResponseHeader = function(headerName) {
+ var header, i, j, key, len, ref, ref1, val;
+ if ((this.responseHeaders == null) && (this.responseHeaderString != null)) {
+ this.responseHeaders = {};
+ ref = this.responseHeaderString.split('\r\n');
+ for (j = 0, len = ref.length; j < len; j++) {
+ header = ref[j];
+ if ((i = header.indexOf(':')) >= 0) {
+ key = header.slice(0, i).trim().toLowerCase();
+ val = header.slice(i + 1).trim();
+ this.responseHeaders[key] = val;
}
- },
- onerror: function() {
- return failure(url);
- },
- onabort: function() {
- return failure(url);
- },
- ontimeout: function() {
- return failure(url);
}
- });
+ }
+ return (ref1 = (this.responseHeaders || {})[headerName.toLowerCase()]) != null ? ref1 : null;
};
+
+ Request.prototype.abort = function() {};
+
+ Request.prototype.onloadend = function() {};
+
+ return Request;
+
})(),
+ ajax: function(url, options) {
+ var gmReq, headers, onloadend, req, timeout;
+ if (options == null) {
+ options = {};
+ }
+ onloadend = options.onloadend, timeout = options.timeout, headers = options.headers;
+ if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) {
+ return $.ajax(url, options);
+ }
+ req = new CrossOrigin.Request();
+ req.onloadend = onloadend;
+ gmReq = ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
+ method: 'GET',
+ url: url,
+ headers: headers,
+ timeout: timeout,
+ onload: function(xhr) {
+ var response;
+ try {
+ response = xhr.responseText ? JSON.parse(xhr.responseText) : null;
+ $.extend(req, {
+ response: response,
+ status: xhr.status,
+ statusText: xhr.statusText,
+ responseHeaderString: xhr.responseHeaders
+ });
+ } catch (_error) {}
+ return req.onloadend();
+ },
+ onerror: function() {
+ return req.onloadend();
+ },
+ onabort: function() {
+ return req.onloadend();
+ },
+ ontimeout: function() {
+ return req.onloadend();
+ }
+ });
+ if (gmReq && typeof gmReq.abort === 'function') {
+ req.abort = function() {
+ try {
+ return gmReq.abort();
+ } catch (_error) {}
+ };
+ }
+ return req;
+ },
+ cache: function(url, cb) {
+ return $.cache(url, cb, {
+ ajax: CrossOrigin.ajax
+ });
+ },
permission: function(cb) {
return cb();
}
@@ -5637,12 +5663,12 @@ Callbacks = (function() {
return this[name] = cb;
};
- Callbacks.prototype.execute = function(node, keys) {
+ Callbacks.prototype.execute = function(node, keys, force) {
var err, errors, i, len, name, ref, ref1, ref2;
if (keys == null) {
keys = this.keys;
}
- if (node.callbacksExecuted) {
+ if (node.callbacksExecuted && !force) {
return;
}
node.callbacksExecuted = true;
@@ -5764,7 +5790,7 @@ DataBoard = (function() {
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
DataBoard = (function() {
- DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles'];
+ DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles'];
function DataBoard(key1, sync, dontClean) {
var init;
@@ -5942,24 +5968,26 @@ DataBoard = (function() {
};
DataBoard.prototype.extend = function(arg, cb) {
- var boardID, postID, rm, siteID, threadID, val;
- siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, postID = arg.postID, val = arg.val, rm = arg.rm;
+ var boardID, postID, siteID, threadID, val;
+ siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, postID = arg.postID, val = arg.val;
return this.save((function(_this) {
return function() {
- var i, key, len, oldVal, ref;
+ var key, oldVal, subVal;
oldVal = _this.get({
siteID: siteID,
boardID: boardID,
threadID: threadID,
postID: postID,
- val: {}
+ defaultValue: {}
});
- ref = rm || [];
- for (i = 0, len = ref.length; i < len; i++) {
- key = ref[i];
- delete oldVal[key];
+ for (key in val) {
+ subVal = val[key];
+ if (typeof subVal === 'undefined') {
+ delete oldVal[key];
+ } else {
+ oldVal[key] = subVal;
+ }
}
- $.extend(oldVal, val);
return _this.setUnsafe({
siteID: siteID,
boardID: boardID,
@@ -5971,10 +5999,13 @@ DataBoard = (function() {
})(this), cb);
};
- DataBoard.prototype.setLastChecked = function() {
+ DataBoard.prototype.setLastChecked = function(key) {
+ if (key == null) {
+ key = 'lastChecked';
+ }
return this.save((function(_this) {
return function() {
- return _this.data.lastChecked = Date.now();
+ return _this.data[key] = Date.now();
};
})(this));
};
@@ -6005,9 +6036,6 @@ DataBoard = (function() {
DataBoard.prototype.clean = function() {
var boardID, now, ref, ref1, siteID, val;
- if (Site.software !== 'yotsuba') {
- return;
- }
siteID = Site.hostname;
ref = this.data[siteID].boards;
for (boardID in ref) {
@@ -6027,21 +6055,36 @@ DataBoard = (function() {
};
DataBoard.prototype.ajaxClean = function(boardID) {
- return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/threads.json", (function(_this) {
- return function(e1) {
- var response1;
- if (e1.target.status !== 200) {
+ var base, siteID, that, threadsList;
+ that = this;
+ siteID = Site.hostname;
+ threadsList = typeof (base = Site.urls).threadsListJSON === "function" ? base.threadsListJSON({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0;
+ if (!threadsList) {
+ return;
+ }
+ return $.cache(threadsList, function() {
+ var archiveList, base1, response1;
+ if (this.status !== 200) {
+ return;
+ }
+ archiveList = typeof (base1 = Site.urls).archiveListJSON === "function" ? base1.archiveListJSON({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0;
+ if (!archiveList) {
+ return that.ajaxCleanParse(boardID, this.response);
+ }
+ response1 = this.response;
+ return $.cache(archiveList, function() {
+ if (this.status !== 200) {
return;
}
- response1 = e1.target.response;
- return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/archive.json", function(e2) {
- if (!(e2.target.status === 200 || (boardID === 'b' || boardID === 'f' || boardID === 'trash' || boardID === 'bant'))) {
- return;
- }
- return _this.ajaxCleanParse(boardID, response1, e2.target.response);
- });
- };
- })(this));
+ return that.ajaxCleanParse(boardID, response1, this.response);
+ });
+ });
};
DataBoard.prototype.ajaxCleanParse = function(boardID, response1, response2) {
@@ -6102,7 +6145,7 @@ Fetcher = (function() {
Fetcher = (function() {
function Fetcher(boardID1, threadID, postID1, root, quoter) {
- var board, post, ref, thread;
+ var board, post, ref, that, thread;
this.boardID = boardID1;
this.threadID = threadID;
this.postID = postID1;
@@ -6122,11 +6165,15 @@ Fetcher = (function() {
}
this.root.textContent = "Loading post No." + this.postID + "...";
if (this.threadID) {
- $.cache(location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json", (function(_this) {
- return function(e, isCached) {
- return _this.fetchedPost(e.target, isCached);
- };
- })(this));
+ that = this;
+ $.cache(Site.urls.threadJSON({
+ boardID: this.boardID,
+ threadID: this.threadID
+ }), function(arg) {
+ var isCached;
+ isCached = arg.isCached;
+ return that.fetchedPost(this, isCached);
+ });
} else {
this.archivedPost();
}
@@ -6165,18 +6212,18 @@ Fetcher = (function() {
};
Fetcher.prototype.fetchedPost = function(req, isCached) {
- var api, board, k, len, post, posts, status, thread;
+ var api, board, k, len, post, posts, status, that, thread;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
}
status = req.status;
if (status !== 200) {
- if (this.archivedPost()) {
+ if (status && this.archivedPost()) {
return;
}
$.addClass(this.root, 'warning');
- this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : "Error " + req.statusText + " (" + req.status + ").";
+ this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : !status ? 'Connection Error' : "Error " + req.statusText + " (" + req.status + ").";
return;
}
posts = req.response.posts;
@@ -6189,15 +6236,17 @@ Fetcher = (function() {
}
if (post.no !== this.postID) {
if (isCached) {
- api = location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json";
+ api = Site.urls.threadJSON({
+ boardID: this.boardID,
+ threadID: this.threadID
+ });
$.cleanCache(function(url) {
return url === api;
});
- $.cache(api, (function(_this) {
- return function(e) {
- return _this.fetchedPost(e.target, false);
- };
- })(this));
+ that = this;
+ $.cache(api, function() {
+ return that.fetchedPost(this, false);
+ });
return;
}
if (this.archivedPost()) {
@@ -6230,7 +6279,7 @@ Fetcher = (function() {
encryptionOK = /^https:\/\//.test(url) || location.protocol === 'http:';
if (encryptionOK || Conf['Exempt Archives from Encryption']) {
that = this;
- CrossOrigin.json(url, function() {
+ CrossOrigin.cache(url, function() {
var key, media, ref, ref1;
if (!encryptionOK && ((ref = this.response) != null ? ref.media : void 0)) {
media = this.response.media;
@@ -6542,6 +6591,7 @@ Post = (function() {
this.ID = +root.id.match(/\d*$/)[0];
this.threadID = this.thread.ID;
this.boardID = this.board.ID;
+ this.siteID = Site.hostname;
this.fullID = this.board + "." + this.ID;
this.context = this;
this.isReply = this.ID !== this.threadID;
@@ -6562,6 +6612,7 @@ Post = (function() {
this.info = {
subject: ((ref1 = this.nodes.subject) != null ? ref1.textContent : void 0) || void 0,
name: (ref2 = this.nodes.name) != null ? ref2.textContent : void 0,
+ email: this.nodes.email ? decodeURIComponent(this.nodes.email.href.replace(/^mailto:/, '')) : void 0,
tripcode: (ref3 = this.nodes.tripcode) != null ? ref3.textContent : void 0,
uniqueID: (ref4 = this.nodes.uniqueID) != null ? ref4.textContent : void 0,
capcode: (ref5 = this.nodes.capcode) != null ? ref5.textContent.replace('## ', '') : void 0,
@@ -7323,7 +7374,7 @@ SW = {};
SW.tinyboard = {
isOPContainerThread: true,
mayLackJSON: true,
- disabledFeatures: ['Board Configuration', 'Normalize URL', 'Captcha Configuration', 'Image Host Rewriting', 'Index Generator', 'Announcement Hiding', 'Fourchan thingies', 'Resurrect Quotes', 'Quick Reply Personas', 'Quick Reply', 'Cooldown', 'Pass Link', 'Index Generator (Menu)', 'Report Link', 'Delete Link', 'Edit Link', 'Archive Link', 'Quote Inlining', 'Quote Previewing', 'Quote Backlinks', 'File Info Formatting', 'Fappe Tyme', 'Image Expansion', 'Image Expansion (Menu)', 'Comment Expansion', 'Thread Expansion', 'Favicon', 'Quote Threading', 'Thread Stats', 'Thread Updater', 'Mark New IPs', 'Banner', 'Flash Features', 'Reply Pruning'],
+ disabledFeatures: ['Board Configuration', 'Normalize URL', 'Captcha Configuration', 'Image Host Rewriting', 'Index Generator', 'Announcement Hiding', 'Resurrect Quotes', 'Quick Reply Personas', 'Quick Reply', 'Cooldown', 'Pass Link', 'Index Generator (Menu)', 'Report Link', 'Delete Link', 'Edit Link', 'Archive Link', 'Quote Inlining', 'Quote Previewing', 'Quote Backlinks', 'File Info Formatting', 'Fappe Tyme', 'Image Expansion', 'Image Expansion (Menu)', 'Comment Expansion', 'Thread Expansion', 'Favicon', 'Quote Threading', 'Thread Stats', 'Thread Updater', 'Mark New IPs', 'Banner', 'Flash Features', 'Reply Pruning'],
detect: function() {
var i, len, m, properties, ref, root, script;
ref = $$('script:not([src])', d.head);
@@ -7359,6 +7410,26 @@ SW = {};
} else {
return '';
}
+ },
+ threadsListJSON: function(arg) {
+ var boardID, ref, root, siteID;
+ siteID = arg.siteID, boardID = arg.boardID;
+ root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0;
+ if (root) {
+ return "" + root + boardID + "/threads.json";
+ } else {
+ return '';
+ }
+ },
+ catalogJSON: function(arg) {
+ var boardID, ref, root, siteID;
+ siteID = arg.siteID, boardID = arg.boardID;
+ root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0;
+ if (root) {
+ return "" + root + boardID + "/catalog.json";
+ } else {
+ return '';
+ }
}
},
selectors: {
@@ -7475,8 +7546,32 @@ SW = {};
var boardID, threadID;
boardID = arg.boardID, threadID = arg.threadID;
return location.protocol + "//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json";
+ },
+ threadsListJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return location.protocol + "//a.4cdn.org/" + boardID + "/threads.json";
+ },
+ archiveListJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ if (BoardConfig.isArchived(boardID)) {
+ return location.protocol + "//a.4cdn.org/" + boardID + "/archive.json";
+ } else {
+ return '';
+ }
+ },
+ catalogJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return location.protocol + "//a.4cdn.org/" + boardID + "/catalog.json";
}
},
+ isPrunedByAge: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return boardID === 'f';
+ },
selectors: {
board: '.board',
thread: '.thread',
@@ -7565,7 +7660,10 @@ SW = {};
thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : void 0;
if (g.BOARD.ID === 'f' && thread.OP.file) {
file = thread.OP.file;
- return $.ajax(location.protocol + "//a.4cdn.org/f/thread/" + thread + ".json", {
+ return $.ajax(Site.urls.threadJSON({
+ boardID: 'f',
+ threadID: thread.ID
+ }), {
timeout: $.MINUTE,
onloadend: function() {
if (this.response) {
@@ -7652,6 +7750,9 @@ SW = {};
},
hasCORS: function(url) {
return url.split('/').slice(0, 3).join('/') === location.protocol + '//a.4cdn.org';
+ },
+ sfwBoards: function(sfw) {
+ return BoardConfig.sfwBoards(sfw);
}
};
@@ -7666,22 +7767,27 @@ Site = (function() {
software: 'yotsuba'
},
'4channel.org': {
- software: 'yotsuba'
+ canonical: '4chan.org'
},
'4cdn.org': {
- software: 'yotsuba'
+ canonical: '4chan.org'
}
},
init: function(cb) {
- var hostname;
+ var canonical, hostname;
$.extend(Conf['siteProperties'], Site.defaultProperties);
hostname = location.hostname;
while (hostname && !(hostname in Conf['siteProperties'])) {
hostname = hostname.replace(/^[^.]*\.?/, '');
}
- if (hostname && Conf['siteProperties'][hostname].software in SW) {
- this.set(hostname);
- cb();
+ if (hostname) {
+ if ((canonical = Conf['siteProperties'][hostname].canonical)) {
+ hostname = canonical;
+ }
+ if (Conf['siteProperties'][hostname].software in SW) {
+ this.set(hostname);
+ cb();
+ }
}
return $.onExists(doc, 'body', (function(_this) {
return function() {
@@ -7717,9 +7823,6 @@ Site = (function() {
this.hostname = hostname1;
this.properties = Conf['siteProperties'][this.hostname];
this.software = this.properties.software;
- if (this.software === 'yotsuba') {
- this.hostname = '4chan.org';
- }
return $.extend(this, SW[this.software]);
}
};
@@ -7860,7 +7963,9 @@ Redirect = (function() {
response: response
});
} else {
- CrossOrigin.json(url, load(i), true);
+ CrossOrigin.ajax(url, {
+ onloadend: load(i)
+ });
}
}
} else {
@@ -8025,15 +8130,13 @@ Filter = (function() {
filters: {},
results: {},
init: function() {
- var base, base1, boards, err, excludes, filter, hl, i, j, key, len, len1, line, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, sfwBoards, stub, top, type, types;
+ var base, base1, boards, err, excludes, file, filter, hide, hl, i, isstring, j, key, len, len1, line, mask, noti, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, regexp, stub, top, type, types;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
if (!Conf['Filtered Backlinks']) {
$.addClass(doc, 'hide-backlinks');
}
- nsfwBoards = BoardConfig.sfwBoards(false).join(',');
- sfwBoards = BoardConfig.sfwBoards(true).join(',');
for (key in Config.filter) {
ref1 = Conf[key].split('\n');
for (i = 0, len = ref1.length; i < len; i++) {
@@ -8041,16 +8144,13 @@ Filter = (function() {
if (line[0] === '#') {
continue;
}
- if (!(regexp = line.match(/\/(.+)\/(\w*)/))) {
+ if (!(regexp = line.match(/\/(.*)\/(\w*)/))) {
continue;
}
filter = line.replace(regexp[0], '');
- boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
- boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards);
- boards = boards === 'global' ? null : boards.split(',');
- excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase() : void 0) || null;
- excludes = excludes === null ? null : excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',');
- if (key === 'uniqueID' || key === 'MD5') {
+ boards = this.parseBoards((ref2 = filter.match(/(?:^|;)\s*boards:([^;]+)/)) != null ? ref2[1] : void 0);
+ excludes = this.parseBoards((ref3 = filter.match(/(?:^|;)\s*exclude:([^;]+)/)) != null ? ref3[1] : void 0);
+ if ((isstring = (key === 'uniqueID' || key === 'MD5'))) {
regexp = regexp[1];
} else {
try {
@@ -8061,10 +8161,19 @@ Filter = (function() {
continue;
}
}
- op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
+ op = ((ref4 = filter.match(/(?:^|;)\s*op:(no|only)/)) != null ? ref4[1] : void 0) || '';
+ mask = {
+ 'no': 1,
+ 'only': 2
+ }[op] || 0;
+ file = ((ref5 = filter.match(/(?:^|;)\s*file:(no|only)/)) != null ? ref5[1] : void 0) || '';
+ mask = mask | ({
+ 'no': 4,
+ 'only': 8
+ }[file] || 0);
stub = (function() {
- var ref5;
- switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
+ var ref6;
+ switch ((ref6 = filter.match(/(?:^|;)\s*stub:(yes|no)/)) != null ? ref6[1] : void 0) {
case 'yes':
return true;
case 'no':
@@ -8073,21 +8182,32 @@ Filter = (function() {
return Conf['Stubs'];
}
})();
- if (hl = /highlight/.test(filter)) {
- hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
- top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
+ noti = /(?:^|;)\s*notify/.test(filter);
+ if ((hl = /(?:^|;)\s*highlight/.test(filter))) {
+ hl = ((ref6 = filter.match(/(?:^|;)\s*highlight:([\w-]+)/)) != null ? ref6[1] : void 0) || 'filter-highlight';
+ top = ((ref7 = filter.match(/(?:^|;)\s*top:(yes|no)/)) != null ? ref7[1] : void 0) || 'yes';
top = top === 'yes';
}
if (key === 'general') {
if ((types = filter.match(/(?:^|;)\s*type:([^;]*)/))) {
- types = types[1].split(',').filter(function(x) {
- return x in Config.filter && x !== 'general';
- });
+ types = types[1].split(',');
} else {
types = ['subject', 'name', 'filename', 'comment'];
}
}
- filter = this.createFilter(regexp, boards, excludes, op, stub, hl, top);
+ hide = !(hl || noti);
+ filter = {
+ isstring: isstring,
+ regexp: regexp,
+ boards: boards,
+ excludes: excludes,
+ mask: mask,
+ hide: hide,
+ stub: stub,
+ hl: hl,
+ top: top,
+ noti: noti
+ };
if (key === 'general') {
for (j = 0, len1 = types.length; j < len1; j++) {
type = types[j];
@@ -8106,37 +8226,45 @@ Filter = (function() {
cb: this.node
});
},
- createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
- var settings, test;
- test = typeof regexp === 'string' ? function(value) {
- return regexp === value;
- } : function(value) {
- return regexp.test(value);
- };
- settings = {
- hide: !hl,
- stub: stub,
- "class": hl,
- top: top
- };
- return function(value, boardID, isReply) {
- if (boards && indexOf.call(boards, boardID) < 0) {
- return false;
+ parseBoards: function(boardsRaw) {
+ var boardID, boardID2, boards, i, j, len, len1, ref, ref1, ref2, ref3, ref4, siteFilter, siteID, siteProperties;
+ if (!boardsRaw) {
+ return false;
+ }
+ if ((boards = Filter.parseBoardsMemo[boardsRaw])) {
+ return boards;
+ }
+ boards = {};
+ siteFilter = '';
+ ref = boardsRaw.split(',');
+ for (i = 0, len = ref.length; i < len; i++) {
+ boardID = ref[i];
+ if (indexOf.call(boardID, ':') >= 0) {
+ ref1 = boardID.split(':').slice(-2), siteFilter = ref1[0], boardID = ref1[1];
}
- if (excludes && indexOf.call(excludes, boardID) >= 0) {
- return false;
+ ref2 = Conf['siteProperties'];
+ for (siteID in ref2) {
+ siteProperties = ref2[siteID];
+ if (siteProperties.canonical || siteID.slice(0, siteFilter.length) !== siteFilter) {
+ continue;
+ }
+ if (boardID === 'nsfw' || boardID === 'sfw') {
+ ref4 = ((ref3 = SW[siteProperties.software]) != null ? typeof ref3.sfwBoards === "function" ? ref3.sfwBoards(boardID === 'sfw') : void 0 : void 0) || [];
+ for (j = 0, len1 = ref4.length; j < len1; j++) {
+ boardID2 = ref4[j];
+ boards[siteID + "/" + boardID2] = true;
+ }
+ } else {
+ boards[siteID + "/" + (encodeURIComponent(boardID))] = true;
+ }
}
- if (isReply && op === 'only' || !isReply && op === 'no') {
- return false;
- }
- if (!test(value)) {
- return false;
- }
- return settings;
- };
+ }
+ Filter.parseBoardsMemo[boardsRaw] = boards;
+ return boards;
},
+ parseBoardsMemo: {},
test: function(post, hideable) {
- var filter, hide, hl, i, key, len, ref, ref1, result, stub, top, value;
+ var board, filter, hide, hl, i, key, len, mask, noti, ref, ref1, site, stub, top, value;
if (hideable == null) {
hideable = true;
}
@@ -8147,25 +8275,34 @@ Filter = (function() {
stub = true;
hl = void 0;
top = false;
+ noti = false;
if (QuoteYou.isYou(post)) {
hideable = false;
}
+ mask = (post.isReply ? 2 : 1);
+ mask = mask | (post.file ? 4 : 8);
+ board = post.siteID + "/" + post.boardID;
+ site = post.siteID + "/*";
for (key in Filter.filters) {
- if (((value = Filter[key](post)) != null)) {
+ if (((value = Filter.value(key, post)) != null)) {
ref = Filter.filters[key];
for (i = 0, len = ref.length; i < len; i++) {
filter = ref[i];
- if ((result = filter(value, post.boardID, post.isReply))) {
- if (result.hide) {
- if (hideable) {
- hide = true;
- stub && (stub = result.stub);
- }
- } else {
- if (!(hl && (ref1 = result["class"], indexOf.call(hl, ref1) >= 0))) {
- (hl || (hl = [])).push(result["class"]);
- }
- top || (top = result.top);
+ if ((filter.boards && !(filter.boards[board] || filter.boards[site])) || (filter.excludes && (filter.excludes[board] || filter.excludes[site])) || (filter.mask & mask) || (filter.isstring ? filter.regexp !== value : !filter.regexp.test(value))) {
+ continue;
+ }
+ if (filter.hide) {
+ if (hideable) {
+ hide = true;
+ stub && (stub = filter.stub);
+ }
+ } else {
+ if (!(hl && (ref1 = filter.hl, indexOf.call(hl, ref1) >= 0))) {
+ (hl || (hl = [])).push(filter.hl);
+ }
+ top || (top = filter.top);
+ if (filter.noti) {
+ noti = true;
}
}
}
@@ -8179,16 +8316,17 @@ Filter = (function() {
} else {
return {
hl: hl,
- top: top
+ top: top,
+ noti: noti
};
}
},
node: function() {
- var hide, hl, ref, stub, top;
+ var hide, hl, noti, ref, stub, top;
if (this.isClone) {
return;
}
- ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top;
+ ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top, noti = ref.noti;
if (hide) {
if (this.isReply) {
PostHiding.hide(this, stub);
@@ -8201,53 +8339,71 @@ Filter = (function() {
$.addClass.apply($, [this.nodes.root].concat(slice.call(hl)));
}
}
+ if (noti && Unread.posts && (this.ID > Unread.lastReadPost) && !QuoteYou.isYou(this)) {
+ return Unread.openNotification(this, ' triggered a notification filter');
+ }
},
isHidden: function(post) {
return !!Filter.test(post).hide;
},
- postID: function(post) {
- return "" + post.ID;
+ valueF: {
+ postID: function(post) {
+ return "" + post.ID;
+ },
+ name: function(post) {
+ return post.info.name;
+ },
+ uniqueID: function(post) {
+ return post.info.uniqueID || '';
+ },
+ tripcode: function(post) {
+ return post.info.tripcode;
+ },
+ capcode: function(post) {
+ return post.info.capcode;
+ },
+ pass: function(post) {
+ return post.info.pass;
+ },
+ email: function(post) {
+ return post.info.email;
+ },
+ subject: function(post) {
+ return post.info.subject || (post.isReply ? void 0 : '');
+ },
+ comment: function(post) {
+ var base;
+ return (base = post.info).comment != null ? base.comment : base.comment = Build.parseComment(post.info.commentHTML.innerHTML);
+ },
+ flag: function(post) {
+ return post.info.flag;
+ },
+ filename: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.name : void 0;
+ },
+ dimensions: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.dimensions : void 0;
+ },
+ filesize: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.size : void 0;
+ },
+ MD5: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.MD5 : void 0;
+ }
},
- name: function(post) {
- return post.info.name;
- },
- uniqueID: function(post) {
- return post.info.uniqueID;
- },
- tripcode: function(post) {
- return post.info.tripcode;
- },
- capcode: function(post) {
- return post.info.capcode;
- },
- pass: function(post) {
- return post.info.pass;
- },
- subject: function(post) {
- return post.info.subject || (post.isReply ? void 0 : '');
- },
- comment: function(post) {
- var base;
- return (base = post.info).comment != null ? base.comment : base.comment = Build.parseComment(post.info.commentHTML.innerHTML);
- },
- flag: function(post) {
- return post.info.flag;
- },
- filename: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.name : void 0;
- },
- dimensions: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.dimensions : void 0;
- },
- filesize: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.size : void 0;
- },
- MD5: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.MD5 : void 0;
+ value: function(key, post) {
+ if (key in Filter.valueF) {
+ return Filter.valueF[key](post);
+ } else {
+ return key.split('+').map(function(k) {
+ var base;
+ return (typeof (base = Filter.valueF)[k] === "function" ? base[k](post) : void 0) || '';
+ }).join('\n');
+ }
},
addFilter: function(type, re, cb) {
return $.get(type, Conf[type], function(item) {
@@ -8303,7 +8459,7 @@ Filter = (function() {
},
subEntries: []
};
- ref1 = [['Name', 'name'], ['Unique ID', 'uniqueID'], ['Tripcode', 'tripcode'], ['Capcode', 'capcode'], ['Pass Date', 'pass'], ['Subject', 'subject'], ['Comment', 'comment'], ['Flag', 'flag'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'MD5']];
+ ref1 = [['Name', 'name'], ['Unique ID', 'uniqueID'], ['Tripcode', 'tripcode'], ['Capcode', 'capcode'], ['Pass Date', 'pass'], ['Email', 'email'], ['Subject', 'subject'], ['Comment', 'comment'], ['Flag', 'flag'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'MD5']];
for (i = 0, len = ref1.length; i < len; i++) {
type = ref1[i];
entry.subEntries.push(Filter.menu.createSubEntry(type[0], type[1]));
@@ -8322,7 +8478,7 @@ Filter = (function() {
el: el,
open: function(post) {
var value;
- value = Filter[type](post);
+ value = Filter.value(type, post);
return value != null;
}
};
@@ -8330,7 +8486,7 @@ Filter = (function() {
makeFilter: function() {
var re, type, value;
type = this.dataset.type;
- value = Filter[type](Filter.menu.post);
+ value = Filter.value(type, Filter.menu.post);
re = type === 'uniqueID' || type === 'MD5' ? value : Filter.escape(value);
re = type === 'uniqueID' || type === 'MD5' ? "/" + re + "/" : "/^" + re + "$/";
return Filter.addFilter(type, re, function() {
@@ -9137,6 +9293,11 @@ BoardConfig = (function() {
domain: function(board) {
return "boards." + (BoardConfig.isSFW(board) ? '4channel' : '4chan') + ".org";
},
+ isArchived: function(board) {
+ var data;
+ data = (this.boards || Conf['boardConfig'].boards)[board];
+ return !data || data.is_archived;
+ },
noAudio: function(boardID) {
var boards;
if (Site.software !== 'yotsuba') {
@@ -9198,25 +9359,30 @@ Build = (function() {
sameThread: function(boardID, threadID) {
return g.VIEW === 'thread' && g.BOARD.ID === boardID && g.THREADID === +threadID;
},
- postURL: function(boardID, threadID, postID) {
- if (Build.sameThread(boardID, threadID)) {
- return "#p" + postID;
+ threadURL: function(boardID, threadID) {
+ if (boardID !== g.BOARD.ID) {
+ return "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/thread/" + threadID;
+ } else if (g.VIEW !== 'thread' || +threadID !== g.THREADID) {
+ return "/" + boardID + "/thread/" + threadID;
} else {
- return "/" + boardID + "/thread/" + threadID + "#p" + postID;
+ return '';
}
},
- parseJSON: function(data, boardID) {
- var o;
+ postURL: function(boardID, threadID, postID) {
+ return (Build.threadURL(boardID, threadID)) + "#p" + postID;
+ },
+ parseJSON: function(data, boardID, siteID) {
+ var key, o;
o = {
ID: data.no,
threadID: data.resto || data.no,
boardID: boardID,
+ siteID: siteID || Site.hostname,
isReply: !!data.resto,
isSticky: !!data.sticky,
isClosed: !!data.closed,
isArchived: !!data.archived,
- fileDeleted: !!data.filedeleted,
- xa18: data.xa18
+ fileDeleted: !!data.filedeleted
};
o.info = {
subject: Build.unescape(data.sub),
@@ -9260,6 +9426,11 @@ Build = (function() {
o.file.dimensions = o.file.width + "x" + o.file.height;
}
}
+ for (key in data) {
+ if (key[0] === 'x') {
+ o[key] = data[key];
+ }
+ }
return o;
},
parseComment: function(html) {
@@ -9282,7 +9453,7 @@ Build = (function() {
return Build.post(o);
},
post: function(o) {
- var ID, boardID, capcode, capcodeDescription, capcodeLC, capcodeLong, capcodePlural, commentHTML, container, dateText, dateUTC, email, file, fileBlock, fileThumb, fileURL, flag, flagCode, flagCodeTroll, gifIcon, href, i, len, match, name, pass, postClass, postInfo, postLink, protocol, quote, quoteLink, ref, ref1, shortFilename, staticPath, subject, threadID, tripcode, uniqueID, wholePost;
+ var ID, boardID, capcode, capcodeDescription, capcodeLC, capcodeLong, capcodePlural, commentHTML, container, dateText, dateUTC, email, file, fileBlock, fileThumb, fileURL, flag, flagCode, flagCodeTroll, gifIcon, href, i, len, match, name, pass, postClass, postInfo, postLink, protocol, quote, quoteLink, ref, ref1, shortFilename, staticPath, subject, threadID, tripcode, uniqueID, url, wholePost;
ID = o.ID, threadID = o.threadID, boardID = o.boardID, file = o.file;
ref = o.info, subject = ref.subject, email = ref.email, name = ref.name, tripcode = ref.tripcode, capcode = ref.capcode, pass = ref.pass, uniqueID = ref.uniqueID, flagCode = ref.flagCode, flagCodeTroll = ref.flagCodeTroll, flag = ref.flag, dateUTC = ref.dateUTC, dateText = ref.dateText, commentHTML = ref.commentHTML;
staticPath = Build.staticPath, gifIcon = Build.gifIcon;
@@ -9305,10 +9476,11 @@ Build = (function() {
capcodeDescription = "a 4chan " + capcodeLong;
}
}
- postLink = Build.postURL(boardID, threadID, ID);
- quoteLink = Build.sameThread(boardID, threadID) ? "javascript:quote('" + (+ID) + "');" : "/" + boardID + "/thread/" + threadID + "#q" + ID;
+ url = Build.threadURL(boardID, threadID);
+ postLink = url + "#p" + ID;
+ quoteLink = Build.sameThread(boardID, threadID) ? "javascript:quote('" + (+ID) + "');" : url + "#q" + ID;
postInfo = {
- innerHTML: "
"
+ innerHTML: ""
};
/* File Info */
@@ -9336,12 +9508,14 @@ Build = (function() {
for (i = 0, len = ref1.length; i < len; i++) {
quote = ref1[i];
href = quote.getAttribute('href');
- if ((href[0] === '#') && !(Build.sameThread(boardID, threadID))) {
- quote.href = ("/" + boardID + "/thread/" + threadID) + href;
- } else if ((match = href.match(/^\/([^\/]+)\/thread\/(\d+)/)) && (Build.sameThread(match[1], match[2]))) {
- quote.href = href.match(/(#[^#]*)?$/)[0] || '#';
- } else if (/^\d+(#|$)/.test(href) && !(g.VIEW === 'thread' && g.BOARD.ID === boardID)) {
- quote.href = "/" + boardID + "/thread/" + href;
+ if (href[0] === '#') {
+ if (!Build.sameThread(boardID, threadID)) {
+ quote.href = Build.threadURL(boardID, threadID) + href;
+ }
+ } else {
+ if ((match = quote.href.match(SW.yotsuba.regexp.quotelink)) && (Build.sameThread(match[1], match[2]))) {
+ quote.href = href.match(/(#[^#]*)?$/)[0] || '#';
+ }
}
}
return container;
@@ -9904,7 +10078,7 @@ Header = (function() {
}
}
if (/-expired/.test(t)) {
- if (boardID !== 'b' && boardID !== 'f' && boardID !== 'trash' && boardID !== 'bant') {
+ if (BoardConfig.isArchived(boardID)) {
a.href = "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/archive";
} else {
return a.firstChild;
@@ -10230,7 +10404,7 @@ Index = (function() {
showHiddenThreads: false,
changed: {},
init: function() {
- var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, select, sortEntry, tRaw, watchSettings;
+ var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, select, sortEntry, tRaw, watchSettings;
if (!(g.VIEW === 'index' && g.BOARD.ID !== 'f')) {
return;
}
@@ -10320,7 +10494,7 @@ Index = (function() {
innerHTML: "Index Catalog Archive Bottom ... × — [Show ] Index Sort Bump order Last reply Last long reply Creation date Reply count File count Image Size Small Large Index Mode Paged Infinite scrolling All threads Catalog "
});
$('.cataloglink a', this.navLinks).href = CatalogLinks.catalog();
- if ((ref5 = g.BOARD.ID) === 'b' || ref5 === 'trash' || ref5 === 'bant') {
+ if (!BoardConfig.isArchived(g.BOARD.ID)) {
$('.archlistlink', this.navLinks).hidden = true;
}
$.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront);
@@ -10339,9 +10513,9 @@ Index = (function() {
$.on(this.selectSort, 'change', this.cb.sort);
$.on(this.selectSize, 'change', $.cb.value);
$.on(this.selectSize, 'change', this.cb.size);
- ref6 = [this.selectMode, this.selectSize];
- for (k = 0, len1 = ref6.length; k < len1; k++) {
- select = ref6[k];
+ ref5 = [this.selectMode, this.selectSize];
+ for (k = 0, len1 = ref5.length; k < len1; k++) {
+ select = ref5[k];
select.value = Conf[select.name];
}
this.selectRev.checked = /-rev$/.test(Index.currentSort);
@@ -10350,12 +10524,12 @@ Index = (function() {
this.lastLongInputs = $$('input', this.lastLongOptions);
this.lastLongThresholds = [0, 0];
this.lastLongOptions.hidden = this.selectSort.value !== 'lastlong';
- ref7 = this.lastLongInputs;
- for (i = l = 0, len2 = ref7.length; l < len2; i = ++l) {
- input = ref7[i];
+ ref6 = this.lastLongInputs;
+ for (i = l = 0, len2 = ref6.length; l < len2; i = ++l) {
+ input = ref6[i];
$.on(input, 'change', this.cb.lastLongThresholds);
tRaw = Conf["Last Long Reply Thresholds " + i];
- input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref8 = tRaw[g.BOARD.ID]) != null ? ref8 : 100 : tRaw;
+ input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref7 = tRaw[g.BOARD.ID]) != null ? ref7 : 100 : tRaw;
}
this.root = $.el('div', {
className: 'board json-index'
@@ -10376,7 +10550,7 @@ Index = (function() {
return d.title = d.title.replace(/\ -\ Page\ \d+/, '');
});
$.onExists(doc, '.board > .thread > .postContainer, .board + *', function() {
- var board, el, len3, m, ref9, timeEl, topNavPos;
+ var board, el, len3, m, ref8, timeEl, topNavPos;
Build.hat = $('.board > .thread > img:first-child');
if (Build.hat) {
g.BOARD.threads.forEach(function(thread) {
@@ -10395,9 +10569,9 @@ Index = (function() {
try {
d.implementation.createDocument(null, null, null).appendChild(board);
} catch (_error) {}
- ref9 = $$('.navLinks');
- for (m = 0, len3 = ref9.length; m < len3; m++) {
- el = ref9[m];
+ ref8 = $$('.navLinks');
+ for (m = 0, len3 = ref8.length; m < len3; m++) {
+ el = ref8[m];
$.rm(el);
}
$.rm($.id('ctrl-top'));
@@ -10969,56 +11143,42 @@ Index = (function() {
return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? '1 hidden thread' : hiddenCount + " hidden threads";
},
update: function(firstTime) {
- var now, ref, ref1;
- if ((ref = Index.req) != null) {
- ref.abort();
+ var oldReq;
+ if ((oldReq = Index.req)) {
+ delete Index.req;
+ oldReq.abort();
}
- if ((ref1 = Index.notice) != null) {
- ref1.close();
- }
- if (Conf['Index Refresh Notifications'] && d.readyState !== 'loading') {
- Index.notice = new Notice('info', 'Refreshing index...');
+ if (Conf['Index Refresh Notifications']) {
+ Index.notice || (Index.notice = new Notice('info', 'Refreshing index...'));
} else {
- now = Date.now();
- $.ready(function() {
- return Index.nTimeout = setTimeout((function() {
- if (Index.req && !Index.notice) {
- return Index.notice = new Notice('info', 'Refreshing index...');
- }
- }), 3 * $.SECOND - (Date.now() - now));
- });
+ Index.nTimeout || (Index.nTimeout = setTimeout(function() {
+ return Index.notice || (Index.notice = new Notice('info', 'Refreshing index...'));
+ }, 3 * $.SECOND));
}
if (!firstTime && d.readyState !== 'loading' && !$('.board + *')) {
location.reload();
return;
}
- Index.req = $.ajax(location.protocol + "//a.4cdn.org/" + g.BOARD + "/catalog.json", {
- onabort: Index.load,
- onloadend: Index.load
- }, {
- whenModified: 'Index'
- });
+ Index.req = $.whenModified(Site.urls.catalogJSON({
+ boardID: g.BOARD.ID
+ }), 'Index', Index.load);
return $.addClass(Index.button, 'fa-spin');
},
- load: function(e) {
- var err, nTimeout, notice, ref, req, timeEl;
+ load: function() {
+ var err, nTimeout, notice, ref, timeEl;
+ if (this !== Index.req) {
+ return;
+ }
$.rmClass(Index.button, 'fa-spin');
- req = Index.req, notice = Index.notice, nTimeout = Index.nTimeout;
+ notice = Index.notice, nTimeout = Index.nTimeout;
if (nTimeout) {
clearTimeout(nTimeout);
}
delete Index.nTimeout;
delete Index.req;
delete Index.notice;
- if (e.type === 'abort') {
- req.onloadend = null;
- if (notice != null) {
- notice.close();
- }
- return;
- }
- if ((ref = req.status) !== 200 && ref !== 304) {
- err = "Index refresh failed. " + (req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error');
+ if ((ref = this.status) !== 200 && ref !== 304) {
+ err = "Index refresh failed. " + (this.status ? "Error " + this.statusText + " (" + this.status + ")" : 'Connection Error');
if (notice) {
notice.setType('warning');
notice.el.lastElementChild.textContent = err;
@@ -11029,9 +11189,9 @@ Index = (function() {
return;
}
try {
- if (req.status === 200) {
- Index.parse(req.response);
- } else if (req.status === 304) {
+ if (this.status === 200) {
+ Index.parse(this.response);
+ } else if (this.status === 304) {
Index.pageLoad();
}
} catch (_error) {
@@ -11056,7 +11216,7 @@ Index = (function() {
}
}
timeEl = $('#index-last-refresh time', Index.navLinks);
- timeEl.dataset.utc = Date.parse(req.getResponseHeader('Last-Modified'));
+ timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified'));
return RelativeDates.update(timeEl);
},
parse: function(pages) {
@@ -11909,20 +12069,27 @@ Settings = (function() {
return $.after($('input[name="Stubs"]', section).parentNode.parentNode, div);
},
"export": function() {
- return $.get(Conf, function(Conf) {
- delete Conf['boardConfig'];
+ var Conf2;
+ Conf2 = {};
+ $.extend(Conf2, Conf);
+ return $.get(Conf2, function(Conf2) {
+ delete Conf2['boardConfig'];
return Settings.downloadExport({
version: g.VERSION,
date: Date.now(),
- Conf: Conf
+ Conf: Conf2
});
});
},
downloadExport: function(data) {
- var a, p;
+ var a, blob, p, url;
+ blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: 'application/json'
+ });
+ url = URL.createObjectURL(blob);
a = $.el('a', {
download: "4chan X v" + g.VERSION + "-" + data.date + ".json",
- href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2)))))
+ href: url
});
p = $('.imp-exp-result', Settings.dialog);
$.rmAll(p);
@@ -12352,6 +12519,11 @@ Settings = (function() {
set('siteProperties', siteProperties);
}
}
+ if (compareString < '00001.00014.00006.00006') {
+ if (data['sauces'] != null) {
+ set('sauces', data['sauces'].replace(/\/\/%\$1\.deviantart\.com\/gallery\/#\/d%\$2;regexp:\/\^\\w\+_by_\(\\w\+\)-d\(\[\\da-z\]\+\)\//g, '//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/'));
+ }
+ }
return changes;
},
loadSettings: function(data, cb) {
@@ -12381,7 +12553,7 @@ Settings = (function() {
filter: function(section) {
var select;
$.extend(section, {
- innerHTML: "Guide General Post number Name Unique ID Tripcode Capcode Pass Date Subject Comment Flag Filename Image dimensions Filesize Image MD5
"
+ innerHTML: "Guide General Post number Name Unique ID Tripcode Capcode Pass Date Email Subject Comment Flag Filename Image dimensions Filesize Image MD5
"
});
select = $('select', section);
$.on(select, 'change', Settings.selectFilter);
@@ -12412,7 +12584,7 @@ Settings = (function() {
};
});
$.extend(div, {
- innerHTML: "Filter is disabled.
Use regular expressions , one per line. Lines starting with a # will be ignored. For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive. MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:Per boards, separate them with commas. It is global if not specified. Use sfw and nsfw to reference all worksafe or not-worksafe boards. For example: boards:a,jp;. In case of a global rule or one that uses sfw/nsfw, select boards to be excluded from the filter. For example: exclude:vg,v;. Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default). For example: op:only;, op:no; or op:yes;. Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`). For example: stub:yes; or stub:no;. Highlight instead of hiding. You can specify a class name to use with a userstyle. For example: highlight; or highlight:wallpaper;. Highlighted OPs will have their threads put on top of the board index by default. For example: top:yes; or top:no;. Filters in the \"General\" section apply to multiple fields, by default subject,name,filename,comment. The fields can be specified with the type option, separated by commas. For example: type:" + E.cat(filterTypes) + ";. Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there. The native catalog has its own separate filter list.
"
+ innerHTML: "Filter is disabled.
Use regular expressions , one per line. Lines starting with a # will be ignored. For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive. MD5 and Unique ID filtering use exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:Per boards, separate them with commas. It is global if not specified. Use sfw and nsfw to reference all worksafe or not-worksafe boards. For example: boards:a,jp;. To specify boards on a particular site, put the beginning of the domain and a slash character before the list. Any initial www. should not be included, and all 4chan domains are considered 4chan.org. For example: boards:4:a,jp,sama:a,z;. An asterisk can be used to specify all boards on a site. For example: boards:4:*;. Select boards to be excluded from the filter. The syntax is the same as for the boards: option above. For example: exclude:vg,v;. Filter OPs only along with their threads (\`only\`) or replies only (\`no\`). For example: op:only; or op:no;. Filter only posts with files (\`only\`) or only posts without files (\`no\`). For example: file:only; or file:no;. Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`). For example: stub:yes; or stub:no;. Highlight instead of hiding. You can specify a class name to use with a userstyle. For example: highlight; or highlight:wallpaper;. Highlighted OPs will have their threads put on top of the board index by default. For example: top:yes; or top:no;. Show a desktop notification instead of hiding. For example: notify;. Filters in the \"General\" section apply to multiple fields, by default subject,name,filename,comment. The fields can be specified with the type option, separated by commas. For example: type:" + E.cat(filterTypes) + ";. Types can also be combined with a + sign; this indicates the filter applies to the given fields joined by newlines. For example: type:filename+filesize+dimensions;. Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there. The native catalog has its own separate filter list.
"
});
return $('.warning', div).hidden = Conf['Filter'];
},
@@ -13119,8 +13291,9 @@ UI = (function() {
};
hoverstart = function(arg) {
- var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
- root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
+ var cb, el, endEvents, height, latestEvent, noRemove, o, rect, ref, root, width;
+ root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, width = arg.width, cb = arg.cb, noRemove = arg.noRemove;
+ rect = root.getBoundingClientRect();
o = {
root: root,
el: el,
@@ -13132,7 +13305,10 @@ UI = (function() {
clientHeight: doc.clientHeight,
clientWidth: doc.clientWidth,
height: height,
- noRemove: noRemove
+ width: width,
+ noRemove: noRemove,
+ clientX: (rect.left + rect.right) / 2,
+ clientY: (rect.top + rect.bottom) / 2
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
@@ -13160,16 +13336,22 @@ UI = (function() {
hoverstart.padding = 25;
hover = function(e) {
- var clientX, clientY, height, left, ref, right, style, threshold, top;
+ var clientX, clientY, height, left, marginX, ref, ref1, right, style, threshold, top, width;
this.latestEvent = e;
height = (this.height || this.el.offsetHeight) + hoverstart.padding;
- clientX = e.clientX, clientY = e.clientY;
+ width = this.width || this.el.offsetWidth;
+ ref = Conf['Follow Cursor'] ? e : this, clientX = ref.clientX, clientY = ref.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
if (!this.isImage) {
threshold = Math.max(threshold, this.clientWidth - 400);
}
- ref = clientX <= threshold ? [clientX + 45 + 'px', ''] : ['', this.clientWidth - clientX + 45 + 'px'], left = ref[0], right = ref[1];
+ marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45;
+ if (this.isImage) {
+ marginX = Math.min(marginX, this.clientWidth - width);
+ }
+ marginX += 'px';
+ ref1 = clientX <= threshold ? [marginX, ''] : ['', marginX], left = ref1[0], right = ref1[1];
style = this.style;
style.top = top + 'px';
style.left = left;
@@ -13878,8 +14060,11 @@ ImageCommon = (function() {
return cb(URL);
}
};
- return $.ajax(location.protocol + "//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", {
- onload: function() {
+ return $.ajax(Site.urls.threadJSON({
+ boardID: post.boardID,
+ threadID: post.threadID
+ }), {
+ onloadend: function() {
var i, len, postObj, ref;
if (this.status === 404) {
post.kill(!post.isClone);
@@ -14272,7 +14457,7 @@ ImageExpand = (function() {
}
},
mouseout: function(e) {
- if (mousedown && e.clientX <= this.getBoundingClientRect().left) {
+ if (((e.buttons & 1) || mousedown) && e.clientX <= this.getBoundingClientRect().left) {
return ImageExpand.toggle(Get.postFromNode(this));
}
}
@@ -14453,7 +14638,7 @@ ImageHover = (function() {
},
mouseover: function(post) {
return function(e) {
- var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
+ var el, error, file, height, isVideo, maxHeight, maxWidth, ref, ref1, scale, width, x;
if (!doc.contains(this)) {
return;
}
@@ -14489,28 +14674,32 @@ ImageHover = (function() {
}
}
}
- ref1 = (function() {
- var i, len, ref1, results;
- ref1 = file.dimensions.split('x');
- results = [];
- for (i = 0, len = ref1.length; i < len; i++) {
- x = ref1[i];
- results.push(+x);
- }
- return results;
- })(), width = ref1[0], height = ref1[1];
- ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
- maxWidth = Math.max(left, doc.clientWidth - right);
- maxHeight = doc.clientHeight - UI.hover.padding;
- scale = Math.min(1, maxWidth / width, maxHeight / height);
- el.style.maxWidth = (scale * width) + "px";
- el.style.maxHeight = (scale * height) + "px";
+ if (file.dimensions) {
+ ref1 = (function() {
+ var i, len, ref1, results;
+ ref1 = file.dimensions.split('x');
+ results = [];
+ for (i = 0, len = ref1.length; i < len; i++) {
+ x = ref1[i];
+ results.push(+x);
+ }
+ return results;
+ })(), width = ref1[0], height = ref1[1];
+ maxWidth = doc.clientWidth;
+ maxHeight = doc.clientHeight - UI.hover.padding;
+ scale = Math.min(1, maxWidth / width, maxHeight / height);
+ width *= scale;
+ height *= scale;
+ el.style.maxWidth = width + "px";
+ el.style.maxHeight = height + "px";
+ }
return UI.hover({
root: this,
el: el,
latestEvent: e,
endEvents: 'mouseout click',
- height: scale * height,
+ height: height,
+ width: width,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@@ -14918,7 +15107,7 @@ Sauce = (function() {
if (!matches) {
return orig;
}
- type = matches[parameter.slice(1)];
+ type = matches[parameter.slice(1)] || '';
} else {
type = Sauce.formatters[parameter](post, ext);
if (type == null) {
@@ -15368,7 +15557,7 @@ Embedding = (function() {
return Embedding.flushTitles(service);
}
} else {
- return CrossOrigin.json(service.api(uid), (function() {
+ return CrossOrigin.cache(service.api(uid), (function() {
return Embedding.cb.title(this, data);
}));
}
@@ -15387,7 +15576,7 @@ Embedding = (function() {
Embedding.cb.title(this, data);
}
};
- return CrossOrigin.json(service.api((function() {
+ return CrossOrigin.cache(service.api((function() {
var j, len, results;
results = [];
for (j = 0, len = queue.length; j < len; j++) {
@@ -15609,7 +15798,7 @@ Embedding = (function() {
hidden: true,
id: "gist-embed-" + (counter++)
});
- CrossOrigin.json("https://api.github.com/gists/" + a.dataset.uid, function() {
+ CrossOrigin.cache("https://api.github.com/gists/" + a.dataset.uid, function() {
el.textContent = Object.values(this.response.files)[0].content;
el.className = 'prettyprint';
$.global(function() {
@@ -16165,7 +16354,7 @@ ArchiveLink = (function() {
} : function(post) {
var typeParam, value;
typeParam = type === 'country' && post.info.flagCodeTroll ? 'tag' : type;
- value = type === 'country' ? post.info.flagCode || post.info.flagCodeTroll : Filter[type](post);
+ value = type === 'country' ? post.info.flagCode || post.info.flagCodeTroll : Filter.value(type, post);
if (!value) {
return false;
}
@@ -16340,18 +16529,21 @@ DeleteLink = (function() {
return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), {
responseType: 'document',
withCredentials: true,
- onload: function() {
+ onloadend: function() {
return DeleteLink.load(link, post, fileOnly, this.response);
},
- onerror: function() {
- return DeleteLink.error(link, post);
- }
- }, {
form: $.formData(form)
});
},
load: function(link, post, fileOnly, resDoc) {
var el, msg;
+ if (!resDoc) {
+ new Notice('warning', 'Connection error, please retry.', 20);
+ if (post.fullID === DeleteLink.post.fullID) {
+ $.on(link, 'click', DeleteLink.toggle);
+ }
+ return;
+ }
link.textContent = DeleteLink.linkText(fileOnly);
if (resDoc.title === '4chan - Banned') {
el = $.el('span', {
@@ -16380,12 +16572,6 @@ DeleteLink = (function() {
}
}
},
- error: function(link, post) {
- new Notice('warning', 'Connection error, please retry.', 20);
- if (post.fullID === DeleteLink.post.fullID) {
- return $.on(link, 'click', DeleteLink.toggle);
- }
- },
cooldown: {
seconds: {},
start: function(post, seconds) {
@@ -16880,7 +17066,7 @@ CatalogLinks = (function() {
if (board == null) {
board = g.BOARD.ID;
}
- if (Conf['External Catalog'] && (board === 'a' || board === 'c' || board === 'g' || board === 'biz' || board === 'k' || board === 'm' || board === 'o' || board === 'p' || board === 'v' || board === 'vg' || board === 'vr' || board === 'w' || board === 'wg' || board === 'cm' || board === '3' || board === 'adv' || board === 'an' || board === 'asp' || board === 'cgl' || board === 'ck' || board === 'co' || board === 'diy' || board === 'fa' || board === 'fit' || board === 'gd' || board === 'int' || board === 'jp' || board === 'lit' || board === 'mlp' || board === 'mu' || board === 'n' || board === 'out' || board === 'po' || board === 'sci' || board === 'sp' || board === 'tg' || board === 'toy' || board === 'trv' || board === 'tv' || board === 'vp' || board === 'wsg' || board === 'x' || board === 'f' || board === 'pol' || board === 's4s' || board === 'lgbt')) {
+ if (Conf['External Catalog'] && (board === '3' || board === 'a' || board === 'adv' || board === 'an' || board === 'asp' || board === 'biz' || board === 'c' || board === 'cgl' || board === 'ck' || board === 'cm' || board === 'co' || board === 'diy' || board === 'f' || board === 'fa' || board === 'fit' || board === 'g' || board === 'gd' || board === 'his' || board === 'i' || board === 'int' || board === 'jp' || board === 'k' || board === 'lgbt' || board === 'lit' || board === 'm' || board === 'mlp' || board === 'mu' || board === 'n' || board === 'news' || board === 'o' || board === 'out' || board === 'p' || board === 'po' || board === 'pol' || board === 's4s' || board === 'sci' || board === 'sp' || board === 'tg' || board === 'toy' || board === 'trv' || board === 'tv' || board === 'v' || board === 'vg' || board === 'vip' || board === 'vp' || board === 'vr' || board === 'w' || board === 'wg' || board === 'wsg' || board === 'wsr' || board === 'x')) {
return "//catalog.neet.tv/" + board + "/";
} else if (Conf['JSON Index'] && Conf['Use 4chan X Catalog']) {
if (((ref = location.hostname) === 'boards.4chan.org' || ref === 'boards.4channel.org') && g.BOARD.ID === board && g.VIEW === 'index') {
@@ -16952,12 +17138,6 @@ ExpandComment = (function() {
if (g.VIEW !== 'index' || !Conf['Comment Expansion'] || Conf['JSON Index']) {
return;
}
- if (g.BOARD.ID === 'g') {
- this.callbacks.push(Fourchan.code);
- }
- if (g.BOARD.ID === 'sci') {
- this.callbacks.push(Fourchan.math);
- }
return Callbacks.Post.push({
name: 'Comment Expansion',
cb: this.node
@@ -16985,7 +17165,10 @@ ExpandComment = (function() {
return;
}
a.textContent = "Post No." + post + " Loading...";
- return $.cache(location.protocol + "//a.4cdn.org" + (a.pathname.split(/\/+/).splice(0, 4).join('/')) + ".json", function() {
+ return $.cache(Site.urls.threadJSON({
+ boardID: post.boardID,
+ threadID: post.threadID
+ }), function() {
return ExpandComment.parse(this, a, post);
});
},
@@ -17003,7 +17186,7 @@ ExpandComment = (function() {
var callback, clone, comment, href, i, j, k, len, len1, len2, postObj, posts, quote, ref, ref1, spoilerRange, status;
status = req.status;
if (status !== 200 && status !== 304) {
- a.textContent = "Error " + req.statusText + " (" + status + ")";
+ a.textContent = status ? "Error " + req.statusText + " (" + status + ")" : 'Connection Error';
return;
}
posts = req.response.posts;
@@ -17084,15 +17267,16 @@ ExpandThread = (function() {
return $.on(a, 'click', ExpandThread.cbToggle);
},
disconnect: function(refresh) {
- var ref, ref1, status, threadID;
+ var oldReq, ref, status, threadID;
if (g.VIEW === 'thread' || !Conf['Thread Expansion']) {
return;
}
ref = ExpandThread.statuses;
for (threadID in ref) {
status = ref[threadID];
- if ((ref1 = status.req) != null) {
- ref1.abort();
+ if ((oldReq = status.req)) {
+ delete status.req;
+ oldReq.abort();
}
delete ExpandThread.statuses[threadID];
}
@@ -17140,17 +17324,24 @@ ExpandThread = (function() {
var status;
ExpandThread.statuses[thread] = status = {};
a.textContent = Build.summaryText.apply(Build, ['...'].concat(slice.call(a.textContent.match(/\d+/g))));
- return status.req = $.cache(location.protocol + "//a.4cdn.org/" + thread.board + "/thread/" + thread + ".json", function() {
+ return status.req = $.cache(Site.urls.threadJSON({
+ boardID: thread.board.ID,
+ threadID: thread.ID
+ }), function() {
+ if (this !== status.req) {
+ return;
+ }
delete status.req;
return ExpandThread.parse(this, thread, a);
});
},
contract: function(thread, a, threadRoot) {
- var filesCount, i, inlined, len, num, postsCount, replies, reply, status;
+ var filesCount, i, inlined, len, num, oldReq, postsCount, replies, reply, status;
status = ExpandThread.statuses[thread];
delete ExpandThread.statuses[thread];
- if (status.req) {
- status.req.abort();
+ if ((oldReq = status.req)) {
+ delete status.req;
+ oldReq.abort();
if (a) {
a.textContent = Build.summaryText.apply(Build, ['+'].concat(slice.call(a.textContent.match(/\d+/g))));
}
@@ -17200,7 +17391,7 @@ ExpandThread = (function() {
parse: function(req, thread, a) {
var a2, filesCount, i, len, post, postData, posts, postsCount, postsRoot, ref, ref1, root;
if ((ref = req.status) !== 200 && ref !== 304) {
- a.textContent = "Error " + req.statusText + " (" + req.status + ")";
+ a.textContent = req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error';
return;
}
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler;
@@ -17449,10 +17640,14 @@ Fourchan = (function() {
Fourchan = {
init: function() {
var ref;
- if ((ref = g.VIEW) !== 'index' && ref !== 'thread' && ref !== 'archive') {
+ if (!(Site.software === 'yotsuba' && ((ref = g.VIEW) === 'index' || ref === 'thread' || ref === 'archive'))) {
return;
}
- if (g.BOARD.ID === 'g') {
+ BoardConfig.ready(this.initBoard);
+ return Main.ready(this.initReady);
+ },
+ initBoard: function() {
+ if (g.BOARD.config.code_tags) {
$.on(window, 'prettyprint:cb', function(e) {
var post, pre;
if (!(post = g.posts[e.detail.ID])) {
@@ -17468,11 +17663,15 @@ Fourchan = (function() {
});
$.globalEval('window.addEventListener(\'prettyprint\', function(e) {\n window.dispatchEvent(new CustomEvent(\'prettyprint:cb\', {\n detail: {\n ID: e.detail.ID,\n i: e.detail.i,\n html: prettyPrintOne(e.detail.html)\n }\n }));\n}, false);');
Callbacks.Post.push({
- name: 'Parse /g/ code',
- cb: this.code
+ name: 'Parse [code] tags',
+ cb: Fourchan.code
});
+ g.posts.forEach(function(post) {
+ return Callbacks.Post.execute(post, ['Parse [code] tags'], true);
+ });
+ ExpandComment.callbacks.push(Fourchan.code);
}
- if (g.BOARD.ID === 'sci') {
+ if (g.BOARD.config.math_tags) {
$.global(function() {
return window.addEventListener('mathjax', function(e) {
if (window.MathJax) {
@@ -17491,20 +17690,24 @@ Fourchan = (function() {
}, false);
});
Callbacks.Post.push({
- name: 'Parse /sci/ math',
- cb: this.math
+ name: 'Parse [math] tags',
+ cb: Fourchan.math
});
+ g.posts.forEach(function(post) {
+ return Callbacks.Post.execute(post, ['Parse [math] tags'], true);
+ });
+ return ExpandComment.callbacks.push(Fourchan.math);
}
- return Main.ready(function() {
- return $.global(function() {
- var j, len, node, ref1;
- window.clickable_ids = false;
- ref1 = document.querySelectorAll('.posteruid, .capcode');
- for (j = 0, len = ref1.length; j < len; j++) {
- node = ref1[j];
- node.removeEventListener('click', window.idClick, false);
- }
- });
+ },
+ initReady: function() {
+ return $.global(function() {
+ var j, len, node, ref;
+ window.clickable_ids = false;
+ ref = document.querySelectorAll('.posteruid, .capcode');
+ for (j = 0, len = ref.length; j < len; j++) {
+ node = ref[j];
+ node.removeEventListener('click', window.idClick, false);
+ }
});
},
code: function() {
@@ -17881,6 +18084,12 @@ Keybinds = (function() {
}
ThreadWatcher.toggleWatcher();
break;
+ case Conf['Toggle threading']:
+ if (!QuoteThreading.ready) {
+ return;
+ }
+ QuoteThreading.toggleThreading();
+ break;
case Conf['Mark thread read']:
if (!(g.VIEW === 'index' && thread && UnreadIndex.enabled)) {
return;
@@ -18735,7 +18944,6 @@ Report = (function() {
results = [];
fn = function(name, url) {
return $.ajax(url, {
- responseType: 'json',
onloadend: function() {
results.push([
name, this.response || {
@@ -18745,8 +18953,7 @@ Report = (function() {
if (results.length === urls.length) {
return cb(results);
}
- }
- }, {
+ },
form: form
});
};
@@ -19378,12 +19585,9 @@ ThreadStats = (function() {
return;
}
ThreadStats.timeout = setTimeout(ThreadStats.fetchPage, 2 * $.MINUTE);
- return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadStats.thread.board + "/threads.json", {
- onload: ThreadStats.onThreadsLoad
- }, {
- whenModified: 'ThreadStats',
- bypassCache: true
- });
+ return $.whenModified(Site.urls.threadsListJSON({
+ boardID: ThreadStats.thread.board
+ }), 'ThreadStats', ThreadStats.onThreadsLoad);
},
onThreadsLoad: function() {
var i, j, k, len, len1, len2, page, purgePos, ref, ref1, ref2, thread;
@@ -19592,11 +19796,12 @@ ThreadUpdater = (function() {
}
},
load: function() {
- var req;
- req = ThreadUpdater.req;
- switch (req.status) {
+ if (this !== ThreadUpdater.req) {
+ return;
+ }
+ switch (this.status) {
case 200:
- ThreadUpdater.parse(req);
+ ThreadUpdater.parse(this);
if (ThreadUpdater.thread.isArchived) {
return ThreadUpdater.kill();
} else {
@@ -19604,7 +19809,9 @@ ThreadUpdater = (function() {
}
break;
case 404:
- return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", {
+ return $.ajax(Site.urls.catalogJSON({
+ boardID: ThreadUpdater.thread.board.ID
+ }), {
onloadend: function() {
var confirmed, i, k, len, len1, page, ref, ref1, thread;
if (this.status === 200) {
@@ -19627,12 +19834,12 @@ ThreadUpdater = (function() {
if (confirmed) {
return ThreadUpdater.kill();
} else {
- return ThreadUpdater.error(req);
+ return ThreadUpdater.error(this);
}
}
});
default:
- return ThreadUpdater.error(req);
+ return ThreadUpdater.error(this);
}
}
},
@@ -19710,18 +19917,18 @@ ThreadUpdater = (function() {
return ThreadUpdater.seconds--;
},
update: function() {
- var ref;
+ var oldReq;
clearTimeout(ThreadUpdater.timeoutID);
ThreadUpdater.set('timer', '...', 'loading');
- if ((ref = ThreadUpdater.req) != null) {
- ref.abort();
+ if ((oldReq = ThreadUpdater.req)) {
+ delete ThreadUpdater.req;
+ oldReq.abort();
}
- return ThreadUpdater.req = $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/thread/" + ThreadUpdater.thread + ".json", {
- onloadend: ThreadUpdater.cb.load,
+ return ThreadUpdater.req = $.whenModified(Site.urls.threadJSON({
+ boardID: ThreadUpdater.thread.board.ID,
+ threadID: ThreadUpdater.thread.ID
+ }), 'ThreadUpdater', ThreadUpdater.cb.load, {
timeout: $.MINUTE
- }, {
- whenModified: 'ThreadUpdater',
- bypassCache: true
});
},
updateThreadStatus: function(type, status) {
@@ -19881,6 +20088,7 @@ ThreadWatcher = (function() {
className: 'fa fa-eye'
});
this.db = new DataBoard('watchedThreads', this.refresh, true);
+ this.dbLM = new DataBoard('watcherLastModified', null, true);
this.dialog = UI.dialog('thread-watcher', {
innerHTML: "
"
});
@@ -19911,6 +20119,7 @@ ThreadWatcher = (function() {
this.dialog.hidden = true;
}
Header.addShortcut('watcher', sc, 510);
+ ThreadWatcher.initLastModified();
ThreadWatcher.fetchAuto();
$.on(window, 'visibilitychange focus', function() {
return $.queueTask(ThreadWatcher.fetchAuto);
@@ -19974,7 +20183,7 @@ ThreadWatcher = (function() {
return toggler.title = (isWatched ? 'Unwatch' : 'Watch') + " Thread";
},
node: function() {
- var boardID, data, threadID, toggler;
+ var boardID, data, siteID, threadID, toggler;
if (this.isReply) {
return;
}
@@ -19987,9 +20196,11 @@ ThreadWatcher = (function() {
});
$.before($('input', this.nodes.info), toggler);
}
+ siteID = Site.hostname;
boardID = this.board.ID;
threadID = this.thread.ID;
data = ThreadWatcher.db.get({
+ siteID: siteID,
boardID: boardID,
threadID: threadID
});
@@ -19998,14 +20209,11 @@ ThreadWatcher = (function() {
if (data && (data.excerpt == null)) {
return $.queueTask((function(_this) {
return function() {
- ThreadWatcher.db.extend({
- boardID: boardID,
- threadID: threadID,
+ return ThreadWatcher.update(siteID, boardID, threadID, {
val: {
excerpt: Get.threadExcerpt(_this.thread)
}
});
- return ThreadWatcher.refresh();
};
})(this));
}
@@ -20039,25 +20247,25 @@ ThreadWatcher = (function() {
},
cb: {
openAll: function() {
- var a, i, len1, ref;
+ var a, j, len1, ref;
if ($.hasClass(this, 'disabled')) {
return;
}
ref = $$('a[title]', ThreadWatcher.list);
- for (i = 0, len1 = ref.length; i < len1; i++) {
- a = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ a = ref[j];
$.open(a.href);
}
return $.event('CloseMenu');
},
pruneDeads: function() {
- var boardID, data, i, len1, ref, ref1, siteID, threadID;
+ var boardID, data, j, len1, ref, ref1, siteID, threadID;
if ($.hasClass(this, 'disabled')) {
return;
}
ref = ThreadWatcher.getAll();
- for (i = 0, len1 = ref.length; i < len1; i++) {
- ref1 = ref[i], siteID = ref1.siteID, boardID = ref1.boardID, threadID = ref1.threadID, data = ref1.data;
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ ref1 = ref[j], siteID = ref1.siteID, boardID = ref1.boardID, threadID = ref1.threadID, data = ref1.data;
if (data.isDead) {
ThreadWatcher.db["delete"]({
siteID: siteID,
@@ -20108,28 +20316,32 @@ ThreadWatcher = (function() {
})) {
continue;
}
- nKilled++;
if (Conf['Auto Prune'] || !(data && typeof data === 'object')) {
db["delete"]({
boardID: boardID,
threadID: threadID
});
+ nKilled++;
+ } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ ThreadWatcher.fetchStatus({
+ siteID: siteID,
+ boardID: boardID,
+ threadID: threadID,
+ data: data
+ });
} else {
db.extend({
boardID: boardID,
threadID: threadID,
val: {
- isDead: true
+ isDead: true,
+ page: void 0,
+ lastPage: void 0,
+ unread: void 0,
+ quotingYou: void 0
}
});
- if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
- ThreadWatcher.fetchStatus({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- data: data
- });
- }
+ nKilled++;
}
}
if (nKilled) {
@@ -20147,6 +20359,38 @@ ThreadWatcher = (function() {
},
requests: [],
fetched: 0,
+ fetch: function(url, arg, args, cb) {
+ var ajax, force, onloadend, ref, req, siteID;
+ siteID = arg.siteID, force = arg.force;
+ if (ThreadWatcher.requests.length === 0) {
+ ThreadWatcher.status.textContent = '...';
+ $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
+ }
+ onloadend = function() {
+ if (this.finished) {
+ return;
+ }
+ this.finished = true;
+ ThreadWatcher.fetched++;
+ if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
+ ThreadWatcher.clearRequests();
+ } else {
+ ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%";
+ }
+ return cb.apply(this, args);
+ };
+ ajax = siteID === Site.hostname ? $.ajax : CrossOrigin.ajax;
+ if (force) {
+ if ((ref = $.lastModified.ThreadWatcher) != null) {
+ delete ref[url];
+ }
+ }
+ req = $.whenModified(url, 'ThreadWatcher', onloadend, {
+ timeout: $.MINUTE,
+ ajax: ajax
+ });
+ return ThreadWatcher.requests.push(req);
+ },
clearRequests: function() {
ThreadWatcher.requests = [];
ThreadWatcher.fetched = 0;
@@ -20154,16 +20398,45 @@ ThreadWatcher = (function() {
return $.rmClass(ThreadWatcher.refreshButton, 'fa-spin');
},
abort: function() {
- var i, len1, ref, req;
+ var j, len1, ref, req;
+ delete ThreadWatcher.syncing;
ref = ThreadWatcher.requests;
- for (i = 0, len1 = ref.length; i < len1; i++) {
- req = ref[i];
- if (req.readyState !== 4) {
- req.abort();
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ req = ref[j];
+ if (!(!req.finished)) {
+ continue;
}
+ req.finished = true;
+ req.abort();
}
return ThreadWatcher.clearRequests();
},
+ initLastModified: function() {
+ var base, boardID, boards, data, date, lm, ref, ref1, siteID, url;
+ lm = ((base = $.lastModified)['ThreadWatcher'] || (base['ThreadWatcher'] = {}));
+ ref = ThreadWatcher.dbLM.data;
+ for (siteID in ref) {
+ boards = ref[siteID];
+ ref1 = boards.boards;
+ for (boardID in ref1) {
+ data = ref1[boardID];
+ if (ThreadWatcher.db.get({
+ siteID: siteID,
+ boardID: boardID
+ })) {
+ for (url in data) {
+ date = data[url];
+ lm[url] = date;
+ }
+ } else {
+ ThreadWatcher.dbLM["delete"]({
+ siteID: siteID,
+ boardID: boardID
+ });
+ }
+ }
+ }
+ },
fetchAuto: function() {
var db, interval, now, ref;
clearTimeout(ThreadWatcher.timeout);
@@ -20171,46 +20444,169 @@ ThreadWatcher = (function() {
return;
}
db = ThreadWatcher.db;
- interval = ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] ? 5 * $.MINUTE : 2 * $.HOUR;
+ interval = Conf['Show Page'] || (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) ? 5 * $.MINUTE : 2 * $.HOUR;
now = Date.now();
if (!((now - interval < (ref = db.data.lastChecked || 0) && ref <= now) || d.hidden || !d.hasFocus())) {
ThreadWatcher.fetchAllStatus();
- db.setLastChecked();
}
return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval);
},
buttonFetchAll: function() {
- if (ThreadWatcher.requests.length) {
+ if (ThreadWatcher.syncing || ThreadWatcher.requests.length) {
return ThreadWatcher.abort();
} else {
return ThreadWatcher.fetchAllStatus();
}
},
fetchAllStatus: function() {
- var db, dbs, i, len1, n, results;
+ var dbi, dbs, j, len1, n, results;
+ ThreadWatcher.status.textContent = '...';
+ $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
+ ThreadWatcher.syncing = true;
dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(function(x) {
return x;
});
n = 0;
results = [];
- for (i = 0, len1 = dbs.length; i < len1; i++) {
- db = dbs[i];
- results.push(db.forceSync(function() {
- var j, len2, thread, threads;
+ for (j = 0, len1 = dbs.length; j < len1; j++) {
+ dbi = dbs[j];
+ results.push(dbi.forceSync(function() {
+ var board, boards, db, deep, k, len2, now, ref;
if ((++n) === dbs.length) {
- threads = ThreadWatcher.getAll();
- for (j = 0, len2 = threads.length; j < len2; j++) {
- thread = threads[j];
- ThreadWatcher.fetchStatus(thread);
+ if (!ThreadWatcher.syncing) {
+ return;
+ }
+ delete ThreadWatcher.syncing;
+ db = ThreadWatcher.db;
+ now = Date.now();
+ deep = !((now - 2 * $.HOUR < (ref = db.data.lastChecked2 || 0) && ref <= now));
+ boards = ThreadWatcher.getAll(true);
+ for (k = 0, len2 = boards.length; k < len2; k++) {
+ board = boards[k];
+ ThreadWatcher.fetchBoard(board, deep);
+ }
+ db.setLastChecked();
+ if (deep) {
+ db.setLastChecked('lastChecked2');
+ }
+ if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
+ return ThreadWatcher.clearRequests();
}
}
}));
}
return results;
},
- fetchStatus: function(thread, force) {
- var base, boardID, data, ref, ref1, req, siteID, software, threadID, url;
- siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data;
+ fetchBoard: function(board, deep) {
+ var base, boardID, force, ref, ref1, ref2, siteID, software, url, urlF;
+ if (!board.some(function(thread) {
+ return !thread.data.isDead;
+ })) {
+ return;
+ }
+ force = Conf['Show Page'] && board.some(function(thread) {
+ return (thread.data.page == null) && !thread.data.isDead && thread.data.last !== -1;
+ });
+ ref = board[0], siteID = ref.siteID, boardID = ref.boardID;
+ software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0;
+ urlF = deep && software === 'tinyboard' ? 'catalogJSON' : 'threadsListJSON';
+ url = (ref2 = SW[software]) != null ? typeof (base = ref2.urls)[urlF] === "function" ? base[urlF]({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0 : void 0;
+ if (!url) {
+ return;
+ }
+ return ThreadWatcher.fetch(url, {
+ siteID: siteID,
+ force: force
+ }, [board, url], ThreadWatcher.parseBoard);
+ },
+ parseBoard: function(board, url) {
+ var boardID, data, i, index, item, j, k, l, lastPage, len1, len2, len3, len4, lmDate, m, modified, nThreads, oldest, page, pageLength, ref, ref1, ref2, ref3, ref4, ref5, ref6, replies, siteID, software, thread, threadID, threads;
+ if (this.status !== 200) {
+ return;
+ }
+ ref = board[0], siteID = ref.siteID, boardID = ref.boardID;
+ software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0;
+ lmDate = this.getResponseHeader('Last-Modified');
+ ThreadWatcher.dbLM.extend({
+ siteID: siteID,
+ boardID: boardID,
+ val: $.item(url, lmDate)
+ });
+ threads = {};
+ pageLength = 0;
+ nThreads = 0;
+ oldest = null;
+ try {
+ pageLength = ((ref2 = this.response[0]) != null ? ref2.threads.length : void 0) || 0;
+ ref3 = this.response;
+ for (i = j = 0, len1 = ref3.length; j < len1; i = ++j) {
+ page = ref3[i];
+ ref4 = page.threads;
+ for (k = 0, len2 = ref4.length; k < len2; k++) {
+ item = ref4[k];
+ threads[item.no] = {
+ page: i + 1,
+ index: nThreads,
+ modified: item.last_modified,
+ replies: item.replies
+ };
+ nThreads++;
+ if ((oldest == null) || item.no < oldest) {
+ oldest = item.no;
+ }
+ }
+ }
+ } catch (_error) {
+ for (l = 0, len3 = board.length; l < len3; l++) {
+ thread = board[l];
+ ThreadWatcher.fetchStatus(thread);
+ }
+ }
+ for (m = 0, len4 = board.length; m < len4; m++) {
+ thread = board[m];
+ threadID = thread.threadID, data = thread.data;
+ if (threads[threadID]) {
+ ref5 = threads[threadID], page = ref5.page, index = ref5.index, modified = ref5.modified, replies = ref5.replies;
+ if (Conf['Show Page']) {
+ lastPage = ((ref6 = SW[software]) != null ? typeof ref6.isPrunedByAge === "function" ? ref6.isPrunedByAge({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0 : void 0) ? threadID === oldest : index >= nThreads - pageLength;
+ ThreadWatcher.update(siteID, boardID, threadID, {
+ page: page,
+ lastPage: lastPage
+ });
+ }
+ if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ if (modified !== data.modified || ((replies != null) && replies !== data.replies)) {
+ ThreadWatcher.db.extend({
+ siteID: siteID,
+ boardID: boardID,
+ threadID: threadID,
+ val: {
+ modified: modified
+ }
+ });
+ ThreadWatcher.fetchStatus(thread);
+ }
+ }
+ } else {
+ if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ ThreadWatcher.fetchStatus(thread);
+ } else {
+ ThreadWatcher.update(siteID, boardID, threadID, {
+ isDead: true
+ });
+ }
+ }
+ }
+ },
+ fetchStatus: function(thread) {
+ var base, boardID, data, force, ref, ref1, siteID, software, threadID, url;
+ siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data, force = thread.force;
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
url = (ref1 = SW[software]) != null ? typeof (base = ref1.urls).threadJSON === "function" ? base.threadJSON({
siteID: siteID,
@@ -20226,54 +20622,21 @@ ThreadWatcher = (function() {
if (data.last === -1) {
return;
}
- if (ThreadWatcher.requests.length === 0) {
- ThreadWatcher.status.textContent = '...';
- $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
- }
- if ((typeof Site.hasCORS === "function" ? Site.hasCORS(url) : void 0) || url.split('/').slice(0, 3).join('/') === location.origin) {
- req = $.ajax(url, {
- onloadend: function() {
- return ThreadWatcher.parseStatus.call(this, thread);
- },
- timeout: $.MINUTE
- }, {
- whenModified: force ? false : 'ThreadWatcher'
- });
- } else {
- req = {
- abort: function() {
- return req.aborted = true;
- }
- };
- CrossOrigin.json(url, function() {
- if (req.aborted) {
- return;
- }
- return ThreadWatcher.parseStatus.call(this, thread);
- }, true, $.MINUTE);
- }
- return ThreadWatcher.requests.push(req);
+ return ThreadWatcher.fetch(url, {
+ siteID: siteID,
+ force: force
+ }, [thread], ThreadWatcher.parseStatus);
},
parseStatus: function(arg) {
- var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, siteID, software, threadID, unread, updated, youOP;
+ var boardID, data, isDead, j, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, replies, siteID, software, threadID, unread, youOP;
siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, data = arg.data;
- ThreadWatcher.fetched++;
- if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
- ThreadWatcher.clearRequests();
- } else {
- ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%";
- }
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
if (this.status === 200 && this.response) {
last = this.response.posts[this.response.posts.length - 1].no;
+ replies = this.response.posts.length - 1;
isDead = !!this.response.posts[0].archived;
if (isDead && Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID
- });
- ThreadWatcher.refresh();
+ ThreadWatcher.rm(siteID, boardID, threadID);
return;
}
if (last === data.last && isDead === data.isDead) {
@@ -20294,8 +20657,8 @@ ThreadWatcher = (function() {
postID: threadID
}) : void 0);
ref2 = this.response.posts;
- for (i = 0, len1 = ref2.length; i < len1; i++) {
- postObj = ref2[i];
+ for (j = 0, len1 = ref2.length; j < len1; j++) {
+ postObj = ref2[j];
if (!(postObj.no > lastReadPost)) {
continue;
}
@@ -20308,7 +20671,7 @@ ThreadWatcher = (function() {
continue;
}
unread++;
- if (!quotingYou && !Conf['Require OP Quote Link'] && youOP && !Filter.isHidden(Build.parseJSON(postObj, boardID))) {
+ if (!quotingYou && !Conf['Require OP Quote Link'] && youOP && !Filter.isHidden(Build.parseJSON(postObj, boardID, siteID))) {
quotingYou = true;
continue;
}
@@ -20329,58 +20692,31 @@ ThreadWatcher = (function() {
break;
}
}
- if (quotesYou && !Filter.isHidden(Build.parseJSON(postObj, boardID))) {
+ if (quotesYou && !Filter.isHidden(Build.parseJSON(postObj, boardID, siteID))) {
quotingYou = true;
}
}
- updated = isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou;
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- last: last,
- isDead: isDead,
- unread: unread,
- quotingYou: quotingYou
- }
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ last: last,
+ replies: replies,
+ isDead: isDead,
+ unread: unread,
+ quotingYou: quotingYou
});
- if (updated) {
- return ThreadWatcher.refresh();
- }
} else if (this.status === 404) {
if (SW[software].mayLackJSON && (data.last == null)) {
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- last: -1
- },
- rm: ['unread', 'quotingYou']
- });
- } else if (Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ last: -1
});
} else {
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- isDead: true
- },
- rm: ['unread', 'quotingYou']
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ isDead: true
});
}
- return ThreadWatcher.refresh();
}
},
- getAll: function() {
- var all, boardID, boards, data, ref, ref1, siteID, threadID, threads;
+ getAll: function(groupByBoard) {
+ var all, boardID, boards, cont, data, ref, ref1, siteID, threadID, threads;
all = [];
ref = ThreadWatcher.db.data;
for (siteID in ref) {
@@ -20391,10 +20727,13 @@ ThreadWatcher = (function() {
if (Conf['Current Board'] && (siteID !== Site.hostname || boardID !== g.BOARD.ID)) {
continue;
}
+ if (groupByBoard) {
+ all.push((cont = []));
+ }
for (threadID in threads) {
data = threads[threadID];
if (data && typeof data === 'object') {
- all.push({
+ (groupByBoard ? cont : all).push({
siteID: siteID,
boardID: boardID,
threadID: threadID,
@@ -20407,7 +20746,7 @@ ThreadWatcher = (function() {
return all;
},
makeLine: function(siteID, boardID, threadID, data) {
- var count, div, excerpt, fullID, link, ref, ref1, software, title, x;
+ var count, div, excerpt, fullID, link, page, ref, ref1, software, title, x;
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
x = $.el('a', {
className: 'fa fa-times',
@@ -20428,6 +20767,13 @@ ThreadWatcher = (function() {
title: excerpt,
className: 'watcher-link'
});
+ if (Conf['Show Page'] && (data.page != null)) {
+ page = $.el('span', {
+ textContent: "[" + data.page + "]",
+ className: 'watcher-page'
+ });
+ $.add(link, page);
+ }
if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] && (data.unread != null)) {
count = $.el('span', {
textContent: "(" + data.unread + ")",
@@ -20450,6 +20796,14 @@ ThreadWatcher = (function() {
if (data.isDead) {
$.addClass(div, 'dead-thread');
}
+ if (Conf['Show Page']) {
+ if (data.lastPage) {
+ $.addClass(div, 'last-page');
+ }
+ if (data.page != null) {
+ div.dataset.page = data.page;
+ }
+ }
if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
if (data.unread === 0) {
$.addClass(div, 'replies-read');
@@ -20465,10 +20819,10 @@ ThreadWatcher = (function() {
return div;
},
setPrefixes: function(threads) {
- var conflicts, conflicts2, i, j, len, len1, len2, prefix, prefixes, siteID, siteID2;
+ var conflicts, conflicts2, j, k, len, len1, len2, prefix, prefixes, siteID, siteID2;
prefixes = {};
- for (i = 0, len1 = threads.length; i < len1; i++) {
- siteID = threads[i].siteID;
+ for (j = 0, len1 = threads.length; j < len1; j++) {
+ siteID = threads[j].siteID;
if (siteID in prefixes) {
continue;
}
@@ -20479,8 +20833,8 @@ ThreadWatcher = (function() {
len++;
prefix = siteID.slice(0, len);
conflicts2 = [];
- for (j = 0, len2 = conflicts.length; j < len2; j++) {
- siteID2 = conflicts[j];
+ for (k = 0, len2 = conflicts.length; k < len2; k++) {
+ siteID2 = conflicts[k];
if (siteID2.slice(0, len) === prefix) {
conflicts2.push(siteID2);
} else if (prefixes[siteID2].length < len) {
@@ -20494,12 +20848,12 @@ ThreadWatcher = (function() {
return ThreadWatcher.prefixes = prefixes;
},
build: function() {
- var boardID, data, i, j, len1, len2, list, nodes, ref, ref1, refresher, siteID, thread, threadID, threads;
+ var boardID, data, j, len1, list, nodes, ref, siteID, thread, threadID, threads;
nodes = [];
threads = ThreadWatcher.getAll();
ThreadWatcher.setPrefixes(threads);
- for (i = 0, len1 = threads.length; i < len1; i++) {
- ref = threads[i], siteID = ref.siteID, boardID = ref.boardID, threadID = ref.threadID, data = ref.data;
+ for (j = 0, len1 = threads.length; j < len1; j++) {
+ ref = threads[j], siteID = ref.siteID, boardID = ref.boardID, threadID = ref.threadID, data = ref.data;
if ((data.excerpt == null) && siteID === Site.hostname && (thread = g.threads[boardID + "." + threadID]) && thread.OP) {
ThreadWatcher.db.extend({
boardID: boardID,
@@ -20514,22 +20868,17 @@ ThreadWatcher = (function() {
list = ThreadWatcher.list;
$.rmAll(list);
$.add(list, nodes);
- ThreadWatcher.refreshIcon();
- ref1 = ThreadWatcher.menu.refreshers;
- for (j = 0, len2 = ref1.length; j < len2; j++) {
- refresher = ref1[j];
- refresher();
- }
+ return ThreadWatcher.refreshIcon();
},
refresh: function() {
ThreadWatcher.build();
g.threads.forEach(function(thread) {
- var i, isWatched, len1, post, ref, toggler;
+ var isWatched, j, len1, post, ref, toggler;
isWatched = ThreadWatcher.isWatched(thread);
if (thread.OP) {
ref = [thread.OP].concat(slice.call(thread.OP.clones));
- for (i = 0, len1 = ref.length; i < len1; i++) {
- post = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ post = ref[j];
if ((toggler = $('.watch-thread-link', post.nodes.info))) {
ThreadWatcher.setToggler(toggler, isWatched);
}
@@ -20546,30 +20895,35 @@ ThreadWatcher = (function() {
}
},
refreshIcon: function() {
- var className, i, len1, ref;
+ var className, j, len1, ref;
ref = ['replies-unread', 'replies-quoting-you'];
- for (i = 0, len1 = ref.length; i < len1; i++) {
- className = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ className = ref[j];
ThreadWatcher.shortcut.classList.toggle(className, !!$("." + className, ThreadWatcher.dialog));
}
},
- update: function(boardID, threadID, newData) {
- var data, key, line, n, newLine, ref, siteID, val;
- siteID = Site.hostname;
+ update: function(siteID, boardID, threadID, newData) {
+ var data, j, key, len1, line, n, newLine, ref, ref1, val;
if (!(data = (ref = ThreadWatcher.db) != null ? ref.get({
+ siteID: siteID,
boardID: boardID,
threadID: threadID
}) : void 0)) {
return;
}
if (newData.isDead && Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- boardID: boardID,
- threadID: threadID
- });
- ThreadWatcher.refresh();
+ ThreadWatcher.rm(siteID, boardID, threadID);
return;
}
+ if (newData.isDead || newData.last === -1) {
+ ref1 = ['page', 'lastPage', 'unread', 'quotingyou'];
+ for (j = 0, len1 = ref1.length; j < len1; j++) {
+ key = ref1[j];
+ if (!(key in newData)) {
+ newData[key] = void 0;
+ }
+ }
+ }
n = 0;
for (key in newData) {
val = newData[key];
@@ -20580,18 +20934,13 @@ ThreadWatcher = (function() {
if (!n) {
return;
}
- if (!(data = ThreadWatcher.db.get({
- boardID: boardID,
- threadID: threadID
- }))) {
- return;
- }
ThreadWatcher.db.extend({
+ siteID: siteID,
boardID: boardID,
threadID: threadID,
val: newData
});
- if (line = $("#watched-threads > [data-site-i-d='" + siteID + "'][data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog)) {
+ if ((line = $("#watched-threads > [data-site-i-d='" + siteID + "'][data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog))) {
newLine = ThreadWatcher.makeLine(siteID, boardID, threadID, data);
$.replace(line, newLine);
return ThreadWatcher.refreshIcon();
@@ -20614,16 +20963,19 @@ ThreadWatcher = (function() {
});
return cb();
}
- if (data.isDead && !((data.unread != null) || (data.quotingYou != null))) {
+ if (data.isDead && !((data.page != null) || (data.lastPage != null) || (data.unread != null) || (data.quotingYou != null))) {
return cb();
}
return ThreadWatcher.db.extend({
boardID: boardID,
threadID: threadID,
val: {
- isDead: true
- },
- rm: ['unread', 'quotingYou']
+ isDead: true,
+ page: void 0,
+ lastPage: void 0,
+ unread: void 0,
+ quotingYou: void 0
+ }
}, cb);
},
toggle: function(thread) {
@@ -20662,19 +21014,24 @@ ThreadWatcher = (function() {
return ThreadWatcher.addRaw(boardID, threadID, data);
},
addRaw: function(boardID, threadID, data) {
+ var thread;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
ThreadWatcher.refresh();
- if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
- return ThreadWatcher.fetchStatus({
- siteID: Site.hostname,
- boardID: boardID,
- threadID: threadID,
- data: data
- }, true);
+ thread = {
+ siteID: Site.hostname,
+ boardID: boardID,
+ threadID: threadID,
+ data: data,
+ force: true
+ };
+ if (Conf['Show Page'] && !data.isDead) {
+ return ThreadWatcher.fetchBoard([thread]);
+ } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ return ThreadWatcher.fetchStatus(thread);
}
},
rm: function(siteID, boardID, threadID) {
@@ -20686,7 +21043,6 @@ ThreadWatcher = (function() {
return ThreadWatcher.refresh();
},
menu: {
- refreshers: [],
init: function() {
var menu;
if (!Conf['Thread Watcher']) {
@@ -20708,73 +21064,61 @@ ThreadWatcher = (function() {
});
Header.menu.addEntry({
el: entryEl,
- order: 60
+ order: 60,
+ open: function() {
+ var addClass, ref, rmClass, text;
+ ref = !!ThreadWatcher.db.get({
+ boardID: g.BOARD.ID,
+ threadID: g.THREADID
+ }) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = ref[0], rmClass = ref[1], text = ref[2];
+ $.addClass(entryEl, addClass);
+ $.rmClass(entryEl, rmClass);
+ entryEl.textContent = text;
+ return true;
+ }
});
- $.on(entryEl, 'click', function() {
+ return $.on(entryEl, 'click', function() {
return ThreadWatcher.toggle(g.threads[g.BOARD + "." + g.THREADID]);
});
- return this.refreshers.push(function() {
- var addClass, ref, rmClass, text;
- ref = $('.current', ThreadWatcher.list) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = ref[0], rmClass = ref[1], text = ref[2];
- $.addClass(entryEl, addClass);
- $.rmClass(entryEl, rmClass);
- return entryEl.textContent = text;
- });
},
addMenuEntries: function() {
- var cb, conf, entries, entry, i, len1, name, ref, ref1, refresh, subEntries;
+ var cb, conf, entries, entry, j, len1, name, open, ref, ref1, text;
entries = [];
entries.push({
+ text: 'Open all threads',
cb: ThreadWatcher.cb.openAll,
- entry: {
- el: $.el('a', {
- textContent: 'Open all threads'
- })
- },
- refresh: function() {
- return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled');
+ open: function() {
+ this.el.classList.toggle('disabled', !ThreadWatcher.list.firstElementChild);
+ return true;
}
});
entries.push({
+ text: 'Prune dead threads',
cb: ThreadWatcher.cb.pruneDeads,
- entry: {
+ open: function() {
+ this.el.classList.toggle('disabled', !$('.dead-thread', ThreadWatcher.list));
+ return true;
+ }
+ });
+ for (j = 0, len1 = entries.length; j < len1; j++) {
+ ref = entries[j], text = ref.text, cb = ref.cb, open = ref.open;
+ entry = {
el: $.el('a', {
- textContent: 'Prune dead threads'
+ textContent: text,
+ href: 'javascript:;'
})
- },
- refresh: function() {
- return ($('.dead-thread', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled');
- }
- });
- subEntries = [];
- ref = Config.threadWatcher;
- for (name in ref) {
- conf = ref[name];
- subEntries.push(this.createSubEntry(name, conf[1]));
- }
- entries.push({
- entry: {
- el: $.el('span', {
- textContent: 'Settings'
- }),
- subEntries: subEntries
- }
- });
- for (i = 0, len1 = entries.length; i < len1; i++) {
- ref1 = entries[i], entry = ref1.entry, cb = ref1.cb, refresh = ref1.refresh;
- if (entry.el.nodeName === 'A') {
- entry.el.href = 'javascript:;';
- }
- if (cb) {
- $.on(entry.el, 'click', cb);
- }
- if (refresh) {
- this.refreshers.push(refresh.bind(entry));
- }
+ };
+ $.on(entry.el, 'click', cb);
+ entry.open = open.bind(entry);
this.menu.addEntry(entry);
}
+ ref1 = Config.threadWatcher;
+ for (name in ref1) {
+ conf = ref1[name];
+ this.addCheckbox(name, conf[1]);
+ }
},
- createSubEntry: function(name, desc) {
+ addCheckbox: function(name, desc) {
var entry, input;
entry = {
type: 'thread watcher',
@@ -20788,13 +21132,13 @@ ThreadWatcher = (function() {
entry.el.title += '\n[Remember Last Read Post is disabled.]';
}
$.on(input, 'change', $.cb.checked);
- if (name === 'Current Board' || name === 'Show Unread Count' || name === 'Show Site Prefix') {
+ if (name === 'Current Board' || name === 'Show Page' || name === 'Show Unread Count' || name === 'Show Site Prefix') {
$.on(input, 'change', ThreadWatcher.refresh);
}
- if (name === 'Show Unread Count' || name === 'Auto Update Thread Watcher') {
+ if (name === 'Show Page' || name === 'Show Unread Count' || name === 'Auto Update Thread Watcher') {
$.on(input, 'change', ThreadWatcher.fetchAuto);
}
- return entry;
+ return this.menu.addEntry(entry);
}
}
};
@@ -20948,12 +21292,15 @@ Unread = (function() {
return;
}
},
- openNotification: function(post) {
+ openNotification: function(post, predicate) {
var notif;
+ if (predicate == null) {
+ predicate = ' replied to you';
+ }
if (!Header.areNotificationsEnabled) {
return;
}
- notif = new Notification(post.info.nameBlock + " replied to you", {
+ notif = new Notification("" + post.info.nameBlock + predicate, {
body: post.commentDisplay(),
icon: Favicon.logo
});
@@ -21083,7 +21430,7 @@ Unread = (function() {
saveThreadWatcherCount: $.debounce(2 * $.SECOND, function() {
$.forceSync('Remember Last Read Post');
if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) {
- return ThreadWatcher.update(Unread.thread.board.ID, Unread.thread.ID, {
+ return ThreadWatcher.update(Site.hostname, Unread.thread.board.ID, Unread.thread.ID, {
isDead: Unread.thread.isDead,
unread: Unread.posts.size,
quotingYou: !!(!Conf['Require OP Quote Link'] && QuoteYou.isYou(Unread.thread.OP) ? Unread.posts.size : Unread.postsQuotingYou.size)
@@ -21216,16 +21563,12 @@ UnreadIndex = (function() {
markRead: function() {
var lastPost, thread;
thread = Get.threadFromNode(this);
- if (Index.enabled) {
- lastPost = Index.lastPost(thread.ID);
- } else {
- lastPost = 0;
- thread.posts.forEach(function(post) {
- if (post.ID > lastPost && !post.isFetchedQuote) {
- return lastPost = post.ID;
- }
- });
- }
+ lastPost = Index.enabled ? Index.lastPost(thread.ID) : 0;
+ thread.posts.forEach(function(post) {
+ if (post.ID > lastPost && !post.isFetchedQuote) {
+ return lastPost = post.ID;
+ }
+ });
UnreadIndex.lastReadPost[thread.fullID] = lastPost;
UnreadIndex.db.set({
boardID: thread.board.ID,
@@ -21234,7 +21577,7 @@ UnreadIndex = (function() {
});
$.rm(UnreadIndex.hr[thread.fullID]);
thread.nodes.root.classList.remove('unread-thread');
- return ThreadWatcher.update(thread.board.ID, thread.ID, {
+ return ThreadWatcher.update(Site.hostname, thread.board.ID, thread.ID, {
unread: 0,
quotingYou: false
});
@@ -22729,7 +23072,7 @@ QR = (function() {
}
},
submit: function(e) {
- var captcha, cb, err, extra, filetag, formData, options, post, ref, thread, threadID;
+ var captcha, cb, err, filetag, formData, options, post, ref, thread, threadID;
if (e != null) {
e.preventDefault();
}
@@ -22799,47 +23142,35 @@ QR = (function() {
options = {
responseType: 'document',
withCredentials: true,
- onload: QR.response,
- onerror: function() {
- delete QR.req;
- if (QR.currentCaptcha) {
- Captcha.cache.save(QR.currentCaptcha);
- }
- delete QR.currentCaptcha;
- post.unlock();
- QR.cooldown.auto = true;
- QR.cooldown.addDelay(post, 2);
- QR.status();
- return QR.error(QR.connectionError());
- }
- };
- extra = {
+ onloadend: QR.response,
form: $.formData(formData)
};
if (Conf['Show Upload Progress']) {
- extra.upCallbacks = {
- onload: function() {
+ options.onprogress = function(e) {
+ var ref1;
+ if (this !== ((ref1 = QR.req) != null ? ref1.upload : void 0)) {
+ return;
+ }
+ if (e.loaded < e.total) {
+ QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%";
+ } else {
QR.req.isUploadFinished = true;
QR.req.progress = '...';
- return QR.status();
- },
- onprogress: function(e) {
- QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%";
- return QR.status();
}
+ return QR.status();
};
}
cb = function(response) {
if (response != null) {
QR.currentCaptcha = response;
if (response.challenge != null) {
- extra.form.append('recaptcha_challenge_field', response.challenge);
- extra.form.append('recaptcha_response_field', response.response);
+ options.form.append('recaptcha_challenge_field', response.challenge);
+ options.form.append('recaptcha_response_field', response.response);
} else {
- extra.form.append('g-recaptcha-response', response.response);
+ options.form.append('g-recaptcha-response', response.response);
}
}
- QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options, extra);
+ QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options);
return QR.req.progress = '...';
};
if (typeof captcha === 'function') {
@@ -22865,23 +23196,24 @@ QR = (function() {
return QR.status();
},
response: function() {
- var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, req, resDoc, seconds, threadID;
- req = QR.req;
+ var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, ref3, seconds, threadID;
+ if (this !== QR.req) {
+ return;
+ }
delete QR.req;
post = QR.posts[0];
post.unlock();
- resDoc = req.response;
- if ((err = resDoc.getElementById('errmsg'))) {
- if ((ref = $('a', err)) != null) {
- ref.target = '_blank';
+ if ((err = (ref = this.response) != null ? ref.getElementById('errmsg') : void 0)) {
+ if ((ref1 = $('a', err)) != null) {
+ ref1.target = '_blank';
}
- } else if ((connErr = resDoc.title !== 'Post successful!')) {
+ } else if ((connErr = !this.response || this.response.title !== 'Post successful!')) {
err = QR.connectionError();
if (QR.currentCaptcha) {
Captcha.cache.save(QR.currentCaptcha);
}
- } else if (req.status !== 200) {
- err = "Error " + req.statusText + " (" + req.status + ")";
+ } else if (this.status !== 200) {
+ err = "Error " + this.statusText + " (" + this.status + ")";
}
delete QR.currentCaptcha;
if (err) {
@@ -22904,13 +23236,13 @@ QR = (function() {
} else {
QR.cooldown.auto = false;
}
- QR.captcha.setup(QR.cooldown.auto && ((ref1 = d.activeElement) === QR.nodes.status || ref1 === d.body));
+ QR.captcha.setup(QR.cooldown.auto && ((ref2 = d.activeElement) === QR.nodes.status || ref2 === d.body));
QR.status();
QR.error(err);
return;
}
- h1 = $('h1', resDoc);
- ref2 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref2[0], threadID = ref2[1], postID = ref2[2];
+ h1 = $('h1', this.response);
+ ref3 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref3[0], threadID = ref3[1], postID = ref3[2];
postID = +postID;
threadID = +threadID || postID;
isReply = threadID !== postID;
@@ -22927,10 +23259,10 @@ QR = (function() {
postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount && isReply;
lastPostToThread = !((function() {
- var j, len, p, ref3;
- ref3 = QR.posts.slice(1);
- for (j = 0, len = ref3.length; j < len; j++) {
- p = ref3[j];
+ var j, len, p, ref4;
+ ref4 = QR.posts.slice(1);
+ for (j = 0, len = ref4.length; j < len; j++) {
+ p = ref4[j];
if (p.thread === post.thread) {
return true;
}
@@ -22981,17 +23313,18 @@ QR = (function() {
} else {
return setTimeout(check, attempts * $.SECOND);
}
- }
- }, {
+ },
+ responseType: 'text',
type: 'HEAD'
});
};
return check();
},
abort: function() {
- if (QR.req && !QR.req.isUploadFinished) {
- QR.req.abort();
+ var oldReq;
+ if ((oldReq = QR.req) && !QR.req.isUploadFinished) {
delete QR.req;
+ oldReq.abort();
if (QR.currentCaptcha) {
Captcha.cache.save(QR.currentCaptcha);
}
@@ -24669,6 +25002,14 @@ QuoteThreading =
parent: {},
children: {},
inserted: {},
+ toggleThreading: function() {
+ return this.setThreadingState(!Conf['Thread Quotes']);
+ },
+ setThreadingState: function(enabled) {
+ this.input.checked = enabled;
+ this.setEnabled.call(this.input);
+ return this.rethread.call(this.input);
+ },
setEnabled: function() {
var other, ref;
if (this.checked) {
@@ -25184,9 +25525,6 @@ Main = (function() {
Main = {
init: function() {
var db, flatten, i, items, j, k, key, len, ref, ref1, ref2, w;
- if (d.body && !$('title', d.head)) {
- return;
- }
try {
w = window;
if ($.platform === 'crx') {
diff --git a/builds/4chan-X-noupdate.crx b/builds/4chan-X-noupdate.crx
index 69e186cc5..a37e401b8 100644
Binary files a/builds/4chan-X-noupdate.crx and b/builds/4chan-X-noupdate.crx differ
diff --git a/builds/4chan-X-noupdate.user.js b/builds/4chan-X-noupdate.user.js
index f99b44b36..57177db19 100644
--- a/builds/4chan-X-noupdate.user.js
+++ b/builds/4chan-X-noupdate.user.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
-// @version 1.14.5.13
+// @version 1.14.7.2
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@@ -198,7 +198,7 @@ docSet = function() {
};
g = {
- VERSION: '1.14.5.13',
+ VERSION: '1.14.7.2',
NAMESPACE: '4chan X.',
boards: {}
};
@@ -241,6 +241,7 @@ Config = (function() {
'JSON Index': [true, 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'],
'Use 4chan X Catalog': [true, 'Link to 4chan X\'s catalog instead of the native 4chan one.', 1],
'Index Refresh Notifications': [false, 'Show a notice at the top of the page when the index is refreshed.', 1],
+ 'Follow Cursor': [true, 'Image Hover and Quote Preview move with the mouse cursor.'],
'Open Threads in New Tab': [false, 'Make links to threads in the index / 4chan X catalog open in a new tab.'],
'External Catalog': [false, 'Link to external catalog instead of the internal one.'],
'Catalog Links': [false, 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'],
@@ -410,6 +411,7 @@ Config = (function() {
'Auto Watch': [true, 'Automatically watch threads you start.'],
'Auto Watch Reply': [true, 'Automatically watch threads you reply to.'],
'Auto Prune': [false, 'Automatically remove dead threads.'],
+ 'Show Page': [true, 'Show what page watched threads are on.'],
'Show Unread Count': [true, 'Show number of unread posts in watched threads.'],
'Show Site Prefix': [true, 'When multiple sites are shown in the thread watcher, add a prefix to board names to distinguish them.'],
'Require OP Quote Link': [false, 'For purposes of thread watcher highlighting, only consider posts with a quote link to the OP as replies to the OP.']
@@ -422,6 +424,7 @@ Config = (function() {
tripcode: "# Filter any tripfag\n#/^!/",
capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for admins:\n#/Admin$/;highlight:admin;op:yes",
pass: "# Filter anyone using since4pass:\n#/./",
+ email: '',
subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only",
comment: "# Filter Stallman copypasta on /g/:\n#/what you\'re refer+ing to as linux/i;boards:g\n# Filter posts with 20 or more quote links:\n#/(?:>>\\d(?:(?!>>\\d)[^])*){20}/\n# Filter posts like T H I S / H / I / S:\n#/^>?\\s?\\w\\s?(\\w)\\s?(\\w)\\s?(\\w).*$[\\s>]+\\1[\\s>]+\\2[\\s>]+\\3/im",
flag: '',
@@ -430,7 +433,7 @@ Config = (function() {
filesize: '',
MD5: ''
},
- sauces: "# Known filename formats:\nhttp://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/\n//%$1.deviantart.com/gallery/#/d%$2;regexp:/^\\w+_by_(\\w+)-d([\\da-z]+)/\n//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/\nhttp://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/\nhttps://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/\n\n# Reverse image search:\nhttps://www.google.com/searchbyimage?image_url=%IMG&safe=off\nhttps://www.yandex.com/images/search?rpt=imageview&img_url=%IMG\n#//tineye.com/search?url=%IMG\n#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights\n\n# Specialized reverse image search:\n//iqdb.org/?url=%IMG\nhttps://trace.moe/?auto&url=%IMG;text:wait\n#//3d.iqdb.org/?url=%IMG\n#//saucenao.com/search.php?url=%IMG\n\n# \"View Same\" in archives:\nhttp://eye.swfchan.com/search/?q=%name;types:swf\n#https://desuarchive.org/_/search/image/%sMD5/\n#https://archive.4plebs.org/_/search/image/%sMD5/\n#https://boards.fireden.net/_/search/image/%sMD5/\n#https://foolz.fireden.net/_/search/image/%sMD5/\n\n# Other tools:\n#http://exif.regex.info/exif.cgi?imgurl=%URL\n#//imgops.com/%URL;types:gif,jpg,png\n#//www.gif-explode.com/%URL;types:gif",
+ sauces: "# Known filename formats:\nhttp://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/\n//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/\n//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/\nhttp://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/\nhttps://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/\n\n# Reverse image search:\nhttps://www.google.com/searchbyimage?image_url=%IMG&safe=off\nhttps://www.yandex.com/images/search?rpt=imageview&img_url=%IMG\n#//tineye.com/search?url=%IMG\n#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights\n\n# Specialized reverse image search:\n//iqdb.org/?url=%IMG\nhttps://trace.moe/?auto&url=%IMG;text:wait\n#//3d.iqdb.org/?url=%IMG\n#//saucenao.com/search.php?url=%IMG\n\n# \"View Same\" in archives:\nhttp://eye.swfchan.com/search/?q=%name;types:swf\n#https://desuarchive.org/_/search/image/%sMD5/\n#https://archive.4plebs.org/_/search/image/%sMD5/\n#https://boards.fireden.net/_/search/image/%sMD5/\n#https://foolz.fireden.net/_/search/image/%sMD5/\n\n# Other tools:\n#http://exif.regex.info/exif.cgi?imgurl=%URL\n#//imgops.com/%URL;types:gif,jpg,png\n#//www.gif-explode.com/%URL;types:gif",
FappeT: {
werk: false
},
@@ -497,6 +500,7 @@ Config = (function() {
'Update': ['r', 'Update the thread / refresh the index.'],
'Update thread watcher': ['Shift+r', 'Manually refresh thread watcher.'],
'Toggle thread watcher': ['t', 'Toggle visibility of thread watcher.'],
+ 'Toggle threading': ['Shift+t', 'Toggle threading.'],
'Mark thread read': ['Ctrl+0', 'Mark thread read from index (requires "Unread Line in Index").'],
'Expand image': ['Shift+e', 'Expand selected image.'],
'Expand images': ['e', 'Expand all images.'],
@@ -1390,6 +1394,10 @@ body.is_catalog .thread > a > img {\n\
.nwsb {\n\
display: inline;\n\
}\n\
+.fileText {\n\
+ max-width: auto;\n\
+ white-space: normal;\n\
+}\n\
/* Ads */\n\
.ad-cnt > *, .adg-rects > *, .bsa-cnt {\n\
height: auto !important;\n\
@@ -2412,12 +2420,11 @@ span.hide-announcement {\n\
-webkit-flex-direction: row;\n\
flex-direction: row;\n\
}\n\
+#watched-threads .watcher-page,\n\
#watched-threads .watcher-unread {\n\
-webkit-flex: 0 0 auto;\n\
flex: 0 0 auto;\n\
-}\n\
-#watched-threads .watcher-unread::after {\n\
- content: \"\\00a0\";\n\
+ margin-right: 2px;\n\
}\n\
#watched-threads .watcher-title {\n\
overflow: hidden;\n\
@@ -2425,7 +2432,10 @@ span.hide-announcement {\n\
-webkit-flex: 0 1 auto;\n\
flex: 0 1 auto;\n\
}\n\
-.replies-quoting-you > a, #watcher-link.replies-quoting-you {\n\
+#watched-threads .watcher-title:not(:first-child) {\n\
+ margin-left: 2px;\n\
+}\n\
+.replies-quoting-you > a, #watcher-link.replies-quoting-you, .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
#thread-watcher a {\n\
@@ -2587,6 +2597,13 @@ span.hide-announcement {\n\
.fileThumb > .warning {\n\
clear: both;\n\
}\n\
+#ihover {\n\
+ pointer-events: none;\n\
+ /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */\n\
+ max-height: 95vh;\n\
+ max-height: calc(100vh - 25px);\n\
+ max-width: 100vw;\n\
+}\n\
/* WEBM Metadata */\n\
.webm-title > a::before {\n\
content: \"title\";\n\
@@ -3699,7 +3716,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(240,224,214,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you {\n\
+:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you, :root.yotsuba .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -3881,7 +3898,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(240,224,214,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you {\n\
+:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you, :root.futaba .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -3974,7 +3991,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(214,218,240,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you {\n\
+:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you, :root.burichan .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4135,7 +4152,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(40,42,46,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you {\n\
+:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you, :root.tomorrow .last-page > a > .watcher-page {\n\
color: #F00 !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4227,7 +4244,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(221,221,221,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you {\n\
+:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you, :root.photon .last-page > a > .watcher-page {\n\
color: #00F !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4382,7 +4399,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(23,21,38,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you {\n\
+:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you, :root.spooky .last-page > a > .watcher-page {\n\
color: #F00 !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4595,58 +4612,43 @@ $ = (function() {
};
$.ajax = (function() {
- var lastModified, pageXHR;
- lastModified = {};
+ var pageXHR;
if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) {
pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest);
} else {
pageXHR = XMLHttpRequest;
}
- return function(url, options, extra) {
- var bypassCache, err, event, form, j, len, params, r, ref, ref1, type, upCallbacks, url0, whenModified;
+ return function(url, options) {
+ var err, form, headers, key, onloadend, onprogress, r, ref, responseType, timeout, type, value, withCredentials;
if (options == null) {
options = {};
}
- if (extra == null) {
- extra = {};
- }
- type = extra.type, whenModified = extra.whenModified, bypassCache = extra.bypassCache, upCallbacks = extra.upCallbacks, form = extra.form;
- if (/\.json$/.test(url)) {
- if (options.responseType == null) {
- options.responseType = 'json';
- }
+ onloadend = options.onloadend, timeout = options.timeout, responseType = options.responseType, withCredentials = options.withCredentials, type = options.type, onprogress = options.onprogress, form = options.form, headers = options.headers;
+ if (responseType == null) {
+ responseType = 'json';
}
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/');
- if (whenModified) {
- params = [];
- if ($.engine === 'blink') {
- params.push("s=" + whenModified);
- }
- if (Site.software === 'yotsuba' && bypassCache) {
- params.push("t=" + (Date.now()));
- }
- url0 = url;
- if (params.length) {
- url += '?' + params.join('&');
- }
- }
r = new pageXHR();
type || (type = form && 'post' || 'get');
try {
r.open(type, url, true);
- if (whenModified) {
- if (((ref = lastModified[whenModified]) != null ? ref[url0] : void 0) != null) {
- r.setRequestHeader('If-Modified-Since', lastModified[whenModified][url0]);
- }
- $.on(r, 'load', function() {
- return (lastModified[whenModified] || (lastModified[whenModified] = {}))[url0] = r.getResponseHeader('Last-Modified');
- });
+ ref = headers || {};
+ for (key in ref) {
+ value = ref[key];
+ r.setRequestHeader(key, value);
}
- $.extend(r, options);
- $.extend(r.upload, upCallbacks);
+ $.extend(r, {
+ onloadend: onloadend,
+ timeout: timeout,
+ responseType: responseType,
+ withCredentials: withCredentials
+ });
+ $.extend(r.upload, {
+ onprogress: onprogress
+ });
$.on(r, 'error', function() {
if (!r.status) {
- return c.error("4chan X failed to load: " + url);
+ return c.warn("4chan X failed to load: " + url);
}
});
r.send(form);
@@ -4655,51 +4657,82 @@ $ = (function() {
if (err.result !== 0x805e0006) {
throw err;
}
- ref1 = ['error', 'loadend'];
- for (j = 0, len = ref1.length; j < len; j++) {
- event = ref1[j];
- r["on" + event] = options["on" + event];
- $.queueTask($.event, event, null, r);
- }
+ r.onloadend = onloadend;
+ $.queueTask($.event, 'error', null, r);
+ $.queueTask($.event, 'loadend', null, r);
}
return r;
};
})();
+ $.lastModified = {};
+
+ $.whenModified = function(url, bucket, cb, options) {
+ var ajax, headers, params, r, ref, t, timeout, url0;
+ if (options == null) {
+ options = {};
+ }
+ timeout = options.timeout, ajax = options.ajax;
+ params = [];
+ if ($.engine === 'blink') {
+ params.push("s=" + bucket);
+ }
+ if (url.split('/')[2] === 'a.4cdn.org') {
+ params.push("t=" + (Date.now()));
+ }
+ url0 = url;
+ if (params.length) {
+ url += '?' + params.join('&');
+ }
+ headers = {};
+ if ((t = (ref = $.lastModified[bucket]) != null ? ref[url0] : void 0) != null) {
+ headers['If-Modified-Since'] = t;
+ }
+ r = (ajax || $.ajax)(url, {
+ onloadend: function() {
+ var base;
+ ((base = $.lastModified)[bucket] || (base[bucket] = {}))[url0] = this.getResponseHeader('Last-Modified');
+ return cb.call(this);
+ },
+ timeout: timeout,
+ headers: headers
+ });
+ return r;
+ };
+
(function() {
var reqs;
reqs = {};
$.cache = function(url, cb, options) {
- var err, req, rm;
- if (req = reqs[url]) {
- if (req.readyState === 4) {
- $.queueTask(function() {
- return cb.call(req, req.evt, true);
- });
- } else {
+ var ajax, onloadend, req;
+ if (options == null) {
+ options = {};
+ }
+ ajax = options.ajax;
+ if ((req = reqs[url])) {
+ if (req.callbacks) {
req.callbacks.push(cb);
+ } else {
+ $.queueTask(function() {
+ return cb.call(req, {
+ isCached: true
+ });
+ });
}
return req;
}
- rm = function() {
- return delete reqs[url];
- };
- try {
- if (!(req = $.ajax(url, options))) {
- return;
- }
- } catch (_error) {
- err = _error;
- return;
- }
- $.on(req, 'load', function(e) {
+ onloadend = function() {
var fn1, j, len, ref;
- this.evt = e;
+ if (!this.status) {
+ delete reqs[url];
+ }
ref = this.callbacks;
fn1 = (function(_this) {
return function(cb) {
return $.queueTask(function() {
- return cb.call(_this, e, false);
+ return cb.call(_this, {
+ isCached: false
+ });
});
};
})(this);
@@ -4708,8 +4741,10 @@ $ = (function() {
fn1(cb);
}
return delete this.callbacks;
+ };
+ req = (ajax || $.ajax)(url, {
+ onloadend: onloadend
});
- $.on(req, 'abort error', rm);
req.callbacks = [cb];
return reqs[url] = req;
};
@@ -5402,25 +5437,25 @@ $$ = (function() {
}).call(this);
CrossOrigin = (function() {
- var CrossOrigin;
+ var CrossOrigin, Request;
CrossOrigin = {
binary: function(url, cb, headers) {
- var options, ref, workaround;
if (headers == null) {
headers = {};
}
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/');
- workaround = $.engine === 'gecko' && (typeof GM_info !== "undefined" && GM_info !== null) && /^[0-2]\.|^3\.[01](?!\d)/.test(GM_info.version);
- workaround || (workaround = /PaleMoon\//.test(navigator.userAgent));
- workaround || (workaround = (typeof GM_info !== "undefined" && GM_info !== null ? (ref = GM_info.script) != null ? ref.includeJSB : void 0 : void 0) != null);
- options = {
+ return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
method: "GET",
url: url,
headers: headers,
+ responseType: 'arraybuffer',
+ overrideMimeType: 'text/plain; charset=x-user-defined',
onload: function(xhr) {
- var contentDisposition, contentType, data, i, r, ref1, ref2;
- if (workaround) {
+ var data, i, r;
+ if (xhr.response instanceof ArrayBuffer) {
+ data = new Uint8Array(xhr.response);
+ } else {
r = xhr.responseText;
data = new Uint8Array(r.length);
i = 0;
@@ -5428,12 +5463,8 @@ CrossOrigin = (function() {
data[i] = r.charCodeAt(i);
i++;
}
- } else {
- data = new Uint8Array(xhr.response);
}
- contentType = (ref1 = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)) != null ? ref1[1] : void 0;
- contentDisposition = (ref2 = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)) != null ? ref2[1] : void 0;
- return cb(data, contentType, contentDisposition);
+ return cb(data, xhr.responseHeaders);
},
onerror: function() {
return cb(null);
@@ -5441,27 +5472,23 @@ CrossOrigin = (function() {
onabort: function() {
return cb(null);
}
- };
- if (workaround) {
- options.overrideMimeType = 'text/plain; charset=x-user-defined';
- } else {
- options.responseType = 'arraybuffer';
- }
- return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)(options);
+ });
},
file: function(url, cb) {
- return CrossOrigin.binary(url, function(data, contentType, contentDisposition) {
- var blob, match, mime, name, ref, ref1, ref2, ref3;
+ return CrossOrigin.binary(url, function(data, headers) {
+ var blob, contentDisposition, contentType, match, mime, name, ref, ref1, ref2, ref3, ref4;
if (data == null) {
return cb(null);
}
- name = (ref = url.match(/([^\/]+)\/*$/)) != null ? ref[1] : void 0;
+ name = (ref = url.match(/([^\/?#]+)\/*(?:$|[?#])/)) != null ? ref[1] : void 0;
+ contentType = (ref1 = headers.match(/Content-Type:\s*(.*)/i)) != null ? ref1[1] : void 0;
+ contentDisposition = (ref2 = headers.match(/Content-Disposition:\s*(.*)/i)) != null ? ref2[1] : void 0;
mime = (contentType != null ? contentType.match(/[^;]*/)[0] : void 0) || 'application/octet-stream';
- match = (contentDisposition != null ? (ref1 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref1[1] : void 0 : void 0) || (contentType != null ? (ref2 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref2[1] : void 0 : void 0);
+ match = (contentDisposition != null ? (ref3 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref3[1] : void 0 : void 0) || (contentType != null ? (ref4 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref4[1] : void 0 : void 0);
if (match) {
name = match.replace(/\\"/g, '"');
}
- if ((typeof GM_info !== "undefined" && GM_info !== null ? (ref3 = GM_info.script) != null ? ref3.includeJSB : void 0 : void 0) != null) {
+ if (/^text\/plain;\s*charset=x-user-defined$/i.test(mime)) {
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] || 'application/octet-stream';
}
blob = new Blob([data], {
@@ -5471,95 +5498,94 @@ CrossOrigin = (function() {
return cb(blob);
});
},
- json: (function() {
- var callbacks, failure, results, success;
- callbacks = {};
- results = {};
- success = function(url, result) {
- var cb, j, len, ref;
- ref = callbacks[url];
- for (j = 0, len = ref.length; j < len; j++) {
- cb = ref[j];
- $.queueTask(function() {
- return cb.call(result);
- });
- }
- delete callbacks[url];
- return results[url] = result;
- };
- failure = function(url) {
- var cb, j, len, ref;
- ref = callbacks[url];
- for (j = 0, len = ref.length; j < len; j++) {
- cb = ref[j];
- $.queueTask(function() {
- return cb.call({});
- });
- }
- return delete callbacks[url];
- };
- return function(url, cb, bypassCache, timeout) {
- var req;
- if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) {
- if (bypassCache) {
- $.cleanCache(function(url2) {
- return url2 === url;
- });
- }
- if ((req = $.cache(url, cb, {
- responseType: 'json'
- }))) {
- $.on(req, 'abort error', function() {
- return cb.call({});
- });
- } else {
- cb.call({});
- }
- return;
- }
- if (bypassCache) {
- delete results[url];
- } else {
- if (results[url]) {
- cb.call(results[url]);
- return;
- }
- if (callbacks[url]) {
- callbacks[url].push(cb);
- return;
- }
- }
- callbacks[url] = [cb];
- return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
- method: "GET",
- url: url + '',
- timeout: timeout,
- onload: function(xhr) {
- var response, status, statusText;
- status = xhr.status, statusText = xhr.statusText;
- try {
- response = JSON.parse(xhr.responseText);
- return success(url, {
- status: status,
- statusText: statusText,
- response: response
- });
- } catch (_error) {
- return failure(url);
+ Request: Request = (function() {
+ function Request() {}
+
+ Request.prototype.status = 0;
+
+ Request.prototype.statusText = '';
+
+ Request.prototype.response = null;
+
+ Request.prototype.responseHeaderString = null;
+
+ Request.prototype.getResponseHeader = function(headerName) {
+ var header, i, j, key, len, ref, ref1, val;
+ if ((this.responseHeaders == null) && (this.responseHeaderString != null)) {
+ this.responseHeaders = {};
+ ref = this.responseHeaderString.split('\r\n');
+ for (j = 0, len = ref.length; j < len; j++) {
+ header = ref[j];
+ if ((i = header.indexOf(':')) >= 0) {
+ key = header.slice(0, i).trim().toLowerCase();
+ val = header.slice(i + 1).trim();
+ this.responseHeaders[key] = val;
}
- },
- onerror: function() {
- return failure(url);
- },
- onabort: function() {
- return failure(url);
- },
- ontimeout: function() {
- return failure(url);
}
- });
+ }
+ return (ref1 = (this.responseHeaders || {})[headerName.toLowerCase()]) != null ? ref1 : null;
};
+
+ Request.prototype.abort = function() {};
+
+ Request.prototype.onloadend = function() {};
+
+ return Request;
+
})(),
+ ajax: function(url, options) {
+ var gmReq, headers, onloadend, req, timeout;
+ if (options == null) {
+ options = {};
+ }
+ onloadend = options.onloadend, timeout = options.timeout, headers = options.headers;
+ if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) {
+ return $.ajax(url, options);
+ }
+ req = new CrossOrigin.Request();
+ req.onloadend = onloadend;
+ gmReq = ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
+ method: 'GET',
+ url: url,
+ headers: headers,
+ timeout: timeout,
+ onload: function(xhr) {
+ var response;
+ try {
+ response = xhr.responseText ? JSON.parse(xhr.responseText) : null;
+ $.extend(req, {
+ response: response,
+ status: xhr.status,
+ statusText: xhr.statusText,
+ responseHeaderString: xhr.responseHeaders
+ });
+ } catch (_error) {}
+ return req.onloadend();
+ },
+ onerror: function() {
+ return req.onloadend();
+ },
+ onabort: function() {
+ return req.onloadend();
+ },
+ ontimeout: function() {
+ return req.onloadend();
+ }
+ });
+ if (gmReq && typeof gmReq.abort === 'function') {
+ req.abort = function() {
+ try {
+ return gmReq.abort();
+ } catch (_error) {}
+ };
+ }
+ return req;
+ },
+ cache: function(url, cb) {
+ return $.cache(url, cb, {
+ ajax: CrossOrigin.ajax
+ });
+ },
permission: function(cb) {
return cb();
}
@@ -5637,12 +5663,12 @@ Callbacks = (function() {
return this[name] = cb;
};
- Callbacks.prototype.execute = function(node, keys) {
+ Callbacks.prototype.execute = function(node, keys, force) {
var err, errors, i, len, name, ref, ref1, ref2;
if (keys == null) {
keys = this.keys;
}
- if (node.callbacksExecuted) {
+ if (node.callbacksExecuted && !force) {
return;
}
node.callbacksExecuted = true;
@@ -5764,7 +5790,7 @@ DataBoard = (function() {
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
DataBoard = (function() {
- DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles'];
+ DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles'];
function DataBoard(key1, sync, dontClean) {
var init;
@@ -5942,24 +5968,26 @@ DataBoard = (function() {
};
DataBoard.prototype.extend = function(arg, cb) {
- var boardID, postID, rm, siteID, threadID, val;
- siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, postID = arg.postID, val = arg.val, rm = arg.rm;
+ var boardID, postID, siteID, threadID, val;
+ siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, postID = arg.postID, val = arg.val;
return this.save((function(_this) {
return function() {
- var i, key, len, oldVal, ref;
+ var key, oldVal, subVal;
oldVal = _this.get({
siteID: siteID,
boardID: boardID,
threadID: threadID,
postID: postID,
- val: {}
+ defaultValue: {}
});
- ref = rm || [];
- for (i = 0, len = ref.length; i < len; i++) {
- key = ref[i];
- delete oldVal[key];
+ for (key in val) {
+ subVal = val[key];
+ if (typeof subVal === 'undefined') {
+ delete oldVal[key];
+ } else {
+ oldVal[key] = subVal;
+ }
}
- $.extend(oldVal, val);
return _this.setUnsafe({
siteID: siteID,
boardID: boardID,
@@ -5971,10 +5999,13 @@ DataBoard = (function() {
})(this), cb);
};
- DataBoard.prototype.setLastChecked = function() {
+ DataBoard.prototype.setLastChecked = function(key) {
+ if (key == null) {
+ key = 'lastChecked';
+ }
return this.save((function(_this) {
return function() {
- return _this.data.lastChecked = Date.now();
+ return _this.data[key] = Date.now();
};
})(this));
};
@@ -6005,9 +6036,6 @@ DataBoard = (function() {
DataBoard.prototype.clean = function() {
var boardID, now, ref, ref1, siteID, val;
- if (Site.software !== 'yotsuba') {
- return;
- }
siteID = Site.hostname;
ref = this.data[siteID].boards;
for (boardID in ref) {
@@ -6027,21 +6055,36 @@ DataBoard = (function() {
};
DataBoard.prototype.ajaxClean = function(boardID) {
- return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/threads.json", (function(_this) {
- return function(e1) {
- var response1;
- if (e1.target.status !== 200) {
+ var base, siteID, that, threadsList;
+ that = this;
+ siteID = Site.hostname;
+ threadsList = typeof (base = Site.urls).threadsListJSON === "function" ? base.threadsListJSON({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0;
+ if (!threadsList) {
+ return;
+ }
+ return $.cache(threadsList, function() {
+ var archiveList, base1, response1;
+ if (this.status !== 200) {
+ return;
+ }
+ archiveList = typeof (base1 = Site.urls).archiveListJSON === "function" ? base1.archiveListJSON({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0;
+ if (!archiveList) {
+ return that.ajaxCleanParse(boardID, this.response);
+ }
+ response1 = this.response;
+ return $.cache(archiveList, function() {
+ if (this.status !== 200) {
return;
}
- response1 = e1.target.response;
- return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/archive.json", function(e2) {
- if (!(e2.target.status === 200 || (boardID === 'b' || boardID === 'f' || boardID === 'trash' || boardID === 'bant'))) {
- return;
- }
- return _this.ajaxCleanParse(boardID, response1, e2.target.response);
- });
- };
- })(this));
+ return that.ajaxCleanParse(boardID, response1, this.response);
+ });
+ });
};
DataBoard.prototype.ajaxCleanParse = function(boardID, response1, response2) {
@@ -6102,7 +6145,7 @@ Fetcher = (function() {
Fetcher = (function() {
function Fetcher(boardID1, threadID, postID1, root, quoter) {
- var board, post, ref, thread;
+ var board, post, ref, that, thread;
this.boardID = boardID1;
this.threadID = threadID;
this.postID = postID1;
@@ -6122,11 +6165,15 @@ Fetcher = (function() {
}
this.root.textContent = "Loading post No." + this.postID + "...";
if (this.threadID) {
- $.cache(location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json", (function(_this) {
- return function(e, isCached) {
- return _this.fetchedPost(e.target, isCached);
- };
- })(this));
+ that = this;
+ $.cache(Site.urls.threadJSON({
+ boardID: this.boardID,
+ threadID: this.threadID
+ }), function(arg) {
+ var isCached;
+ isCached = arg.isCached;
+ return that.fetchedPost(this, isCached);
+ });
} else {
this.archivedPost();
}
@@ -6165,18 +6212,18 @@ Fetcher = (function() {
};
Fetcher.prototype.fetchedPost = function(req, isCached) {
- var api, board, k, len, post, posts, status, thread;
+ var api, board, k, len, post, posts, status, that, thread;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
}
status = req.status;
if (status !== 200) {
- if (this.archivedPost()) {
+ if (status && this.archivedPost()) {
return;
}
$.addClass(this.root, 'warning');
- this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : "Error " + req.statusText + " (" + req.status + ").";
+ this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : !status ? 'Connection Error' : "Error " + req.statusText + " (" + req.status + ").";
return;
}
posts = req.response.posts;
@@ -6189,15 +6236,17 @@ Fetcher = (function() {
}
if (post.no !== this.postID) {
if (isCached) {
- api = location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json";
+ api = Site.urls.threadJSON({
+ boardID: this.boardID,
+ threadID: this.threadID
+ });
$.cleanCache(function(url) {
return url === api;
});
- $.cache(api, (function(_this) {
- return function(e) {
- return _this.fetchedPost(e.target, false);
- };
- })(this));
+ that = this;
+ $.cache(api, function() {
+ return that.fetchedPost(this, false);
+ });
return;
}
if (this.archivedPost()) {
@@ -6230,7 +6279,7 @@ Fetcher = (function() {
encryptionOK = /^https:\/\//.test(url) || location.protocol === 'http:';
if (encryptionOK || Conf['Exempt Archives from Encryption']) {
that = this;
- CrossOrigin.json(url, function() {
+ CrossOrigin.cache(url, function() {
var key, media, ref, ref1;
if (!encryptionOK && ((ref = this.response) != null ? ref.media : void 0)) {
media = this.response.media;
@@ -6542,6 +6591,7 @@ Post = (function() {
this.ID = +root.id.match(/\d*$/)[0];
this.threadID = this.thread.ID;
this.boardID = this.board.ID;
+ this.siteID = Site.hostname;
this.fullID = this.board + "." + this.ID;
this.context = this;
this.isReply = this.ID !== this.threadID;
@@ -6562,6 +6612,7 @@ Post = (function() {
this.info = {
subject: ((ref1 = this.nodes.subject) != null ? ref1.textContent : void 0) || void 0,
name: (ref2 = this.nodes.name) != null ? ref2.textContent : void 0,
+ email: this.nodes.email ? decodeURIComponent(this.nodes.email.href.replace(/^mailto:/, '')) : void 0,
tripcode: (ref3 = this.nodes.tripcode) != null ? ref3.textContent : void 0,
uniqueID: (ref4 = this.nodes.uniqueID) != null ? ref4.textContent : void 0,
capcode: (ref5 = this.nodes.capcode) != null ? ref5.textContent.replace('## ', '') : void 0,
@@ -7323,7 +7374,7 @@ SW = {};
SW.tinyboard = {
isOPContainerThread: true,
mayLackJSON: true,
- disabledFeatures: ['Board Configuration', 'Normalize URL', 'Captcha Configuration', 'Image Host Rewriting', 'Index Generator', 'Announcement Hiding', 'Fourchan thingies', 'Resurrect Quotes', 'Quick Reply Personas', 'Quick Reply', 'Cooldown', 'Pass Link', 'Index Generator (Menu)', 'Report Link', 'Delete Link', 'Edit Link', 'Archive Link', 'Quote Inlining', 'Quote Previewing', 'Quote Backlinks', 'File Info Formatting', 'Fappe Tyme', 'Image Expansion', 'Image Expansion (Menu)', 'Comment Expansion', 'Thread Expansion', 'Favicon', 'Quote Threading', 'Thread Stats', 'Thread Updater', 'Mark New IPs', 'Banner', 'Flash Features', 'Reply Pruning'],
+ disabledFeatures: ['Board Configuration', 'Normalize URL', 'Captcha Configuration', 'Image Host Rewriting', 'Index Generator', 'Announcement Hiding', 'Resurrect Quotes', 'Quick Reply Personas', 'Quick Reply', 'Cooldown', 'Pass Link', 'Index Generator (Menu)', 'Report Link', 'Delete Link', 'Edit Link', 'Archive Link', 'Quote Inlining', 'Quote Previewing', 'Quote Backlinks', 'File Info Formatting', 'Fappe Tyme', 'Image Expansion', 'Image Expansion (Menu)', 'Comment Expansion', 'Thread Expansion', 'Favicon', 'Quote Threading', 'Thread Stats', 'Thread Updater', 'Mark New IPs', 'Banner', 'Flash Features', 'Reply Pruning'],
detect: function() {
var i, len, m, properties, ref, root, script;
ref = $$('script:not([src])', d.head);
@@ -7359,6 +7410,26 @@ SW = {};
} else {
return '';
}
+ },
+ threadsListJSON: function(arg) {
+ var boardID, ref, root, siteID;
+ siteID = arg.siteID, boardID = arg.boardID;
+ root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0;
+ if (root) {
+ return "" + root + boardID + "/threads.json";
+ } else {
+ return '';
+ }
+ },
+ catalogJSON: function(arg) {
+ var boardID, ref, root, siteID;
+ siteID = arg.siteID, boardID = arg.boardID;
+ root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0;
+ if (root) {
+ return "" + root + boardID + "/catalog.json";
+ } else {
+ return '';
+ }
}
},
selectors: {
@@ -7475,8 +7546,32 @@ SW = {};
var boardID, threadID;
boardID = arg.boardID, threadID = arg.threadID;
return location.protocol + "//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json";
+ },
+ threadsListJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return location.protocol + "//a.4cdn.org/" + boardID + "/threads.json";
+ },
+ archiveListJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ if (BoardConfig.isArchived(boardID)) {
+ return location.protocol + "//a.4cdn.org/" + boardID + "/archive.json";
+ } else {
+ return '';
+ }
+ },
+ catalogJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return location.protocol + "//a.4cdn.org/" + boardID + "/catalog.json";
}
},
+ isPrunedByAge: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return boardID === 'f';
+ },
selectors: {
board: '.board',
thread: '.thread',
@@ -7565,7 +7660,10 @@ SW = {};
thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : void 0;
if (g.BOARD.ID === 'f' && thread.OP.file) {
file = thread.OP.file;
- return $.ajax(location.protocol + "//a.4cdn.org/f/thread/" + thread + ".json", {
+ return $.ajax(Site.urls.threadJSON({
+ boardID: 'f',
+ threadID: thread.ID
+ }), {
timeout: $.MINUTE,
onloadend: function() {
if (this.response) {
@@ -7652,6 +7750,9 @@ SW = {};
},
hasCORS: function(url) {
return url.split('/').slice(0, 3).join('/') === location.protocol + '//a.4cdn.org';
+ },
+ sfwBoards: function(sfw) {
+ return BoardConfig.sfwBoards(sfw);
}
};
@@ -7666,22 +7767,27 @@ Site = (function() {
software: 'yotsuba'
},
'4channel.org': {
- software: 'yotsuba'
+ canonical: '4chan.org'
},
'4cdn.org': {
- software: 'yotsuba'
+ canonical: '4chan.org'
}
},
init: function(cb) {
- var hostname;
+ var canonical, hostname;
$.extend(Conf['siteProperties'], Site.defaultProperties);
hostname = location.hostname;
while (hostname && !(hostname in Conf['siteProperties'])) {
hostname = hostname.replace(/^[^.]*\.?/, '');
}
- if (hostname && Conf['siteProperties'][hostname].software in SW) {
- this.set(hostname);
- cb();
+ if (hostname) {
+ if ((canonical = Conf['siteProperties'][hostname].canonical)) {
+ hostname = canonical;
+ }
+ if (Conf['siteProperties'][hostname].software in SW) {
+ this.set(hostname);
+ cb();
+ }
}
return $.onExists(doc, 'body', (function(_this) {
return function() {
@@ -7717,9 +7823,6 @@ Site = (function() {
this.hostname = hostname1;
this.properties = Conf['siteProperties'][this.hostname];
this.software = this.properties.software;
- if (this.software === 'yotsuba') {
- this.hostname = '4chan.org';
- }
return $.extend(this, SW[this.software]);
}
};
@@ -7860,7 +7963,9 @@ Redirect = (function() {
response: response
});
} else {
- CrossOrigin.json(url, load(i), true);
+ CrossOrigin.ajax(url, {
+ onloadend: load(i)
+ });
}
}
} else {
@@ -8025,15 +8130,13 @@ Filter = (function() {
filters: {},
results: {},
init: function() {
- var base, base1, boards, err, excludes, filter, hl, i, j, key, len, len1, line, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, sfwBoards, stub, top, type, types;
+ var base, base1, boards, err, excludes, file, filter, hide, hl, i, isstring, j, key, len, len1, line, mask, noti, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, regexp, stub, top, type, types;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
if (!Conf['Filtered Backlinks']) {
$.addClass(doc, 'hide-backlinks');
}
- nsfwBoards = BoardConfig.sfwBoards(false).join(',');
- sfwBoards = BoardConfig.sfwBoards(true).join(',');
for (key in Config.filter) {
ref1 = Conf[key].split('\n');
for (i = 0, len = ref1.length; i < len; i++) {
@@ -8041,16 +8144,13 @@ Filter = (function() {
if (line[0] === '#') {
continue;
}
- if (!(regexp = line.match(/\/(.+)\/(\w*)/))) {
+ if (!(regexp = line.match(/\/(.*)\/(\w*)/))) {
continue;
}
filter = line.replace(regexp[0], '');
- boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
- boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards);
- boards = boards === 'global' ? null : boards.split(',');
- excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase() : void 0) || null;
- excludes = excludes === null ? null : excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',');
- if (key === 'uniqueID' || key === 'MD5') {
+ boards = this.parseBoards((ref2 = filter.match(/(?:^|;)\s*boards:([^;]+)/)) != null ? ref2[1] : void 0);
+ excludes = this.parseBoards((ref3 = filter.match(/(?:^|;)\s*exclude:([^;]+)/)) != null ? ref3[1] : void 0);
+ if ((isstring = (key === 'uniqueID' || key === 'MD5'))) {
regexp = regexp[1];
} else {
try {
@@ -8061,10 +8161,19 @@ Filter = (function() {
continue;
}
}
- op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
+ op = ((ref4 = filter.match(/(?:^|;)\s*op:(no|only)/)) != null ? ref4[1] : void 0) || '';
+ mask = {
+ 'no': 1,
+ 'only': 2
+ }[op] || 0;
+ file = ((ref5 = filter.match(/(?:^|;)\s*file:(no|only)/)) != null ? ref5[1] : void 0) || '';
+ mask = mask | ({
+ 'no': 4,
+ 'only': 8
+ }[file] || 0);
stub = (function() {
- var ref5;
- switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
+ var ref6;
+ switch ((ref6 = filter.match(/(?:^|;)\s*stub:(yes|no)/)) != null ? ref6[1] : void 0) {
case 'yes':
return true;
case 'no':
@@ -8073,21 +8182,32 @@ Filter = (function() {
return Conf['Stubs'];
}
})();
- if (hl = /highlight/.test(filter)) {
- hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
- top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
+ noti = /(?:^|;)\s*notify/.test(filter);
+ if ((hl = /(?:^|;)\s*highlight/.test(filter))) {
+ hl = ((ref6 = filter.match(/(?:^|;)\s*highlight:([\w-]+)/)) != null ? ref6[1] : void 0) || 'filter-highlight';
+ top = ((ref7 = filter.match(/(?:^|;)\s*top:(yes|no)/)) != null ? ref7[1] : void 0) || 'yes';
top = top === 'yes';
}
if (key === 'general') {
if ((types = filter.match(/(?:^|;)\s*type:([^;]*)/))) {
- types = types[1].split(',').filter(function(x) {
- return x in Config.filter && x !== 'general';
- });
+ types = types[1].split(',');
} else {
types = ['subject', 'name', 'filename', 'comment'];
}
}
- filter = this.createFilter(regexp, boards, excludes, op, stub, hl, top);
+ hide = !(hl || noti);
+ filter = {
+ isstring: isstring,
+ regexp: regexp,
+ boards: boards,
+ excludes: excludes,
+ mask: mask,
+ hide: hide,
+ stub: stub,
+ hl: hl,
+ top: top,
+ noti: noti
+ };
if (key === 'general') {
for (j = 0, len1 = types.length; j < len1; j++) {
type = types[j];
@@ -8106,37 +8226,45 @@ Filter = (function() {
cb: this.node
});
},
- createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
- var settings, test;
- test = typeof regexp === 'string' ? function(value) {
- return regexp === value;
- } : function(value) {
- return regexp.test(value);
- };
- settings = {
- hide: !hl,
- stub: stub,
- "class": hl,
- top: top
- };
- return function(value, boardID, isReply) {
- if (boards && indexOf.call(boards, boardID) < 0) {
- return false;
+ parseBoards: function(boardsRaw) {
+ var boardID, boardID2, boards, i, j, len, len1, ref, ref1, ref2, ref3, ref4, siteFilter, siteID, siteProperties;
+ if (!boardsRaw) {
+ return false;
+ }
+ if ((boards = Filter.parseBoardsMemo[boardsRaw])) {
+ return boards;
+ }
+ boards = {};
+ siteFilter = '';
+ ref = boardsRaw.split(',');
+ for (i = 0, len = ref.length; i < len; i++) {
+ boardID = ref[i];
+ if (indexOf.call(boardID, ':') >= 0) {
+ ref1 = boardID.split(':').slice(-2), siteFilter = ref1[0], boardID = ref1[1];
}
- if (excludes && indexOf.call(excludes, boardID) >= 0) {
- return false;
+ ref2 = Conf['siteProperties'];
+ for (siteID in ref2) {
+ siteProperties = ref2[siteID];
+ if (siteProperties.canonical || siteID.slice(0, siteFilter.length) !== siteFilter) {
+ continue;
+ }
+ if (boardID === 'nsfw' || boardID === 'sfw') {
+ ref4 = ((ref3 = SW[siteProperties.software]) != null ? typeof ref3.sfwBoards === "function" ? ref3.sfwBoards(boardID === 'sfw') : void 0 : void 0) || [];
+ for (j = 0, len1 = ref4.length; j < len1; j++) {
+ boardID2 = ref4[j];
+ boards[siteID + "/" + boardID2] = true;
+ }
+ } else {
+ boards[siteID + "/" + (encodeURIComponent(boardID))] = true;
+ }
}
- if (isReply && op === 'only' || !isReply && op === 'no') {
- return false;
- }
- if (!test(value)) {
- return false;
- }
- return settings;
- };
+ }
+ Filter.parseBoardsMemo[boardsRaw] = boards;
+ return boards;
},
+ parseBoardsMemo: {},
test: function(post, hideable) {
- var filter, hide, hl, i, key, len, ref, ref1, result, stub, top, value;
+ var board, filter, hide, hl, i, key, len, mask, noti, ref, ref1, site, stub, top, value;
if (hideable == null) {
hideable = true;
}
@@ -8147,25 +8275,34 @@ Filter = (function() {
stub = true;
hl = void 0;
top = false;
+ noti = false;
if (QuoteYou.isYou(post)) {
hideable = false;
}
+ mask = (post.isReply ? 2 : 1);
+ mask = mask | (post.file ? 4 : 8);
+ board = post.siteID + "/" + post.boardID;
+ site = post.siteID + "/*";
for (key in Filter.filters) {
- if (((value = Filter[key](post)) != null)) {
+ if (((value = Filter.value(key, post)) != null)) {
ref = Filter.filters[key];
for (i = 0, len = ref.length; i < len; i++) {
filter = ref[i];
- if ((result = filter(value, post.boardID, post.isReply))) {
- if (result.hide) {
- if (hideable) {
- hide = true;
- stub && (stub = result.stub);
- }
- } else {
- if (!(hl && (ref1 = result["class"], indexOf.call(hl, ref1) >= 0))) {
- (hl || (hl = [])).push(result["class"]);
- }
- top || (top = result.top);
+ if ((filter.boards && !(filter.boards[board] || filter.boards[site])) || (filter.excludes && (filter.excludes[board] || filter.excludes[site])) || (filter.mask & mask) || (filter.isstring ? filter.regexp !== value : !filter.regexp.test(value))) {
+ continue;
+ }
+ if (filter.hide) {
+ if (hideable) {
+ hide = true;
+ stub && (stub = filter.stub);
+ }
+ } else {
+ if (!(hl && (ref1 = filter.hl, indexOf.call(hl, ref1) >= 0))) {
+ (hl || (hl = [])).push(filter.hl);
+ }
+ top || (top = filter.top);
+ if (filter.noti) {
+ noti = true;
}
}
}
@@ -8179,16 +8316,17 @@ Filter = (function() {
} else {
return {
hl: hl,
- top: top
+ top: top,
+ noti: noti
};
}
},
node: function() {
- var hide, hl, ref, stub, top;
+ var hide, hl, noti, ref, stub, top;
if (this.isClone) {
return;
}
- ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top;
+ ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top, noti = ref.noti;
if (hide) {
if (this.isReply) {
PostHiding.hide(this, stub);
@@ -8201,53 +8339,71 @@ Filter = (function() {
$.addClass.apply($, [this.nodes.root].concat(slice.call(hl)));
}
}
+ if (noti && Unread.posts && (this.ID > Unread.lastReadPost) && !QuoteYou.isYou(this)) {
+ return Unread.openNotification(this, ' triggered a notification filter');
+ }
},
isHidden: function(post) {
return !!Filter.test(post).hide;
},
- postID: function(post) {
- return "" + post.ID;
+ valueF: {
+ postID: function(post) {
+ return "" + post.ID;
+ },
+ name: function(post) {
+ return post.info.name;
+ },
+ uniqueID: function(post) {
+ return post.info.uniqueID || '';
+ },
+ tripcode: function(post) {
+ return post.info.tripcode;
+ },
+ capcode: function(post) {
+ return post.info.capcode;
+ },
+ pass: function(post) {
+ return post.info.pass;
+ },
+ email: function(post) {
+ return post.info.email;
+ },
+ subject: function(post) {
+ return post.info.subject || (post.isReply ? void 0 : '');
+ },
+ comment: function(post) {
+ var base;
+ return (base = post.info).comment != null ? base.comment : base.comment = Build.parseComment(post.info.commentHTML.innerHTML);
+ },
+ flag: function(post) {
+ return post.info.flag;
+ },
+ filename: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.name : void 0;
+ },
+ dimensions: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.dimensions : void 0;
+ },
+ filesize: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.size : void 0;
+ },
+ MD5: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.MD5 : void 0;
+ }
},
- name: function(post) {
- return post.info.name;
- },
- uniqueID: function(post) {
- return post.info.uniqueID;
- },
- tripcode: function(post) {
- return post.info.tripcode;
- },
- capcode: function(post) {
- return post.info.capcode;
- },
- pass: function(post) {
- return post.info.pass;
- },
- subject: function(post) {
- return post.info.subject || (post.isReply ? void 0 : '');
- },
- comment: function(post) {
- var base;
- return (base = post.info).comment != null ? base.comment : base.comment = Build.parseComment(post.info.commentHTML.innerHTML);
- },
- flag: function(post) {
- return post.info.flag;
- },
- filename: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.name : void 0;
- },
- dimensions: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.dimensions : void 0;
- },
- filesize: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.size : void 0;
- },
- MD5: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.MD5 : void 0;
+ value: function(key, post) {
+ if (key in Filter.valueF) {
+ return Filter.valueF[key](post);
+ } else {
+ return key.split('+').map(function(k) {
+ var base;
+ return (typeof (base = Filter.valueF)[k] === "function" ? base[k](post) : void 0) || '';
+ }).join('\n');
+ }
},
addFilter: function(type, re, cb) {
return $.get(type, Conf[type], function(item) {
@@ -8303,7 +8459,7 @@ Filter = (function() {
},
subEntries: []
};
- ref1 = [['Name', 'name'], ['Unique ID', 'uniqueID'], ['Tripcode', 'tripcode'], ['Capcode', 'capcode'], ['Pass Date', 'pass'], ['Subject', 'subject'], ['Comment', 'comment'], ['Flag', 'flag'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'MD5']];
+ ref1 = [['Name', 'name'], ['Unique ID', 'uniqueID'], ['Tripcode', 'tripcode'], ['Capcode', 'capcode'], ['Pass Date', 'pass'], ['Email', 'email'], ['Subject', 'subject'], ['Comment', 'comment'], ['Flag', 'flag'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'MD5']];
for (i = 0, len = ref1.length; i < len; i++) {
type = ref1[i];
entry.subEntries.push(Filter.menu.createSubEntry(type[0], type[1]));
@@ -8322,7 +8478,7 @@ Filter = (function() {
el: el,
open: function(post) {
var value;
- value = Filter[type](post);
+ value = Filter.value(type, post);
return value != null;
}
};
@@ -8330,7 +8486,7 @@ Filter = (function() {
makeFilter: function() {
var re, type, value;
type = this.dataset.type;
- value = Filter[type](Filter.menu.post);
+ value = Filter.value(type, Filter.menu.post);
re = type === 'uniqueID' || type === 'MD5' ? value : Filter.escape(value);
re = type === 'uniqueID' || type === 'MD5' ? "/" + re + "/" : "/^" + re + "$/";
return Filter.addFilter(type, re, function() {
@@ -9137,6 +9293,11 @@ BoardConfig = (function() {
domain: function(board) {
return "boards." + (BoardConfig.isSFW(board) ? '4channel' : '4chan') + ".org";
},
+ isArchived: function(board) {
+ var data;
+ data = (this.boards || Conf['boardConfig'].boards)[board];
+ return !data || data.is_archived;
+ },
noAudio: function(boardID) {
var boards;
if (Site.software !== 'yotsuba') {
@@ -9198,25 +9359,30 @@ Build = (function() {
sameThread: function(boardID, threadID) {
return g.VIEW === 'thread' && g.BOARD.ID === boardID && g.THREADID === +threadID;
},
- postURL: function(boardID, threadID, postID) {
- if (Build.sameThread(boardID, threadID)) {
- return "#p" + postID;
+ threadURL: function(boardID, threadID) {
+ if (boardID !== g.BOARD.ID) {
+ return "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/thread/" + threadID;
+ } else if (g.VIEW !== 'thread' || +threadID !== g.THREADID) {
+ return "/" + boardID + "/thread/" + threadID;
} else {
- return "/" + boardID + "/thread/" + threadID + "#p" + postID;
+ return '';
}
},
- parseJSON: function(data, boardID) {
- var o;
+ postURL: function(boardID, threadID, postID) {
+ return (Build.threadURL(boardID, threadID)) + "#p" + postID;
+ },
+ parseJSON: function(data, boardID, siteID) {
+ var key, o;
o = {
ID: data.no,
threadID: data.resto || data.no,
boardID: boardID,
+ siteID: siteID || Site.hostname,
isReply: !!data.resto,
isSticky: !!data.sticky,
isClosed: !!data.closed,
isArchived: !!data.archived,
- fileDeleted: !!data.filedeleted,
- xa18: data.xa18
+ fileDeleted: !!data.filedeleted
};
o.info = {
subject: Build.unescape(data.sub),
@@ -9260,6 +9426,11 @@ Build = (function() {
o.file.dimensions = o.file.width + "x" + o.file.height;
}
}
+ for (key in data) {
+ if (key[0] === 'x') {
+ o[key] = data[key];
+ }
+ }
return o;
},
parseComment: function(html) {
@@ -9282,7 +9453,7 @@ Build = (function() {
return Build.post(o);
},
post: function(o) {
- var ID, boardID, capcode, capcodeDescription, capcodeLC, capcodeLong, capcodePlural, commentHTML, container, dateText, dateUTC, email, file, fileBlock, fileThumb, fileURL, flag, flagCode, flagCodeTroll, gifIcon, href, i, len, match, name, pass, postClass, postInfo, postLink, protocol, quote, quoteLink, ref, ref1, shortFilename, staticPath, subject, threadID, tripcode, uniqueID, wholePost;
+ var ID, boardID, capcode, capcodeDescription, capcodeLC, capcodeLong, capcodePlural, commentHTML, container, dateText, dateUTC, email, file, fileBlock, fileThumb, fileURL, flag, flagCode, flagCodeTroll, gifIcon, href, i, len, match, name, pass, postClass, postInfo, postLink, protocol, quote, quoteLink, ref, ref1, shortFilename, staticPath, subject, threadID, tripcode, uniqueID, url, wholePost;
ID = o.ID, threadID = o.threadID, boardID = o.boardID, file = o.file;
ref = o.info, subject = ref.subject, email = ref.email, name = ref.name, tripcode = ref.tripcode, capcode = ref.capcode, pass = ref.pass, uniqueID = ref.uniqueID, flagCode = ref.flagCode, flagCodeTroll = ref.flagCodeTroll, flag = ref.flag, dateUTC = ref.dateUTC, dateText = ref.dateText, commentHTML = ref.commentHTML;
staticPath = Build.staticPath, gifIcon = Build.gifIcon;
@@ -9305,10 +9476,11 @@ Build = (function() {
capcodeDescription = "a 4chan " + capcodeLong;
}
}
- postLink = Build.postURL(boardID, threadID, ID);
- quoteLink = Build.sameThread(boardID, threadID) ? "javascript:quote('" + (+ID) + "');" : "/" + boardID + "/thread/" + threadID + "#q" + ID;
+ url = Build.threadURL(boardID, threadID);
+ postLink = url + "#p" + ID;
+ quoteLink = Build.sameThread(boardID, threadID) ? "javascript:quote('" + (+ID) + "');" : url + "#q" + ID;
postInfo = {
- innerHTML: ""
+ innerHTML: ""
};
/* File Info */
@@ -9336,12 +9508,14 @@ Build = (function() {
for (i = 0, len = ref1.length; i < len; i++) {
quote = ref1[i];
href = quote.getAttribute('href');
- if ((href[0] === '#') && !(Build.sameThread(boardID, threadID))) {
- quote.href = ("/" + boardID + "/thread/" + threadID) + href;
- } else if ((match = href.match(/^\/([^\/]+)\/thread\/(\d+)/)) && (Build.sameThread(match[1], match[2]))) {
- quote.href = href.match(/(#[^#]*)?$/)[0] || '#';
- } else if (/^\d+(#|$)/.test(href) && !(g.VIEW === 'thread' && g.BOARD.ID === boardID)) {
- quote.href = "/" + boardID + "/thread/" + href;
+ if (href[0] === '#') {
+ if (!Build.sameThread(boardID, threadID)) {
+ quote.href = Build.threadURL(boardID, threadID) + href;
+ }
+ } else {
+ if ((match = quote.href.match(SW.yotsuba.regexp.quotelink)) && (Build.sameThread(match[1], match[2]))) {
+ quote.href = href.match(/(#[^#]*)?$/)[0] || '#';
+ }
}
}
return container;
@@ -9904,7 +10078,7 @@ Header = (function() {
}
}
if (/-expired/.test(t)) {
- if (boardID !== 'b' && boardID !== 'f' && boardID !== 'trash' && boardID !== 'bant') {
+ if (BoardConfig.isArchived(boardID)) {
a.href = "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/archive";
} else {
return a.firstChild;
@@ -10230,7 +10404,7 @@ Index = (function() {
showHiddenThreads: false,
changed: {},
init: function() {
- var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, select, sortEntry, tRaw, watchSettings;
+ var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, select, sortEntry, tRaw, watchSettings;
if (!(g.VIEW === 'index' && g.BOARD.ID !== 'f')) {
return;
}
@@ -10320,7 +10494,7 @@ Index = (function() {
innerHTML: "Index Catalog Archive Bottom ... × — [Show ] Index Sort Bump order Last reply Last long reply Creation date Reply count File count Image Size Small Large Index Mode Paged Infinite scrolling All threads Catalog "
});
$('.cataloglink a', this.navLinks).href = CatalogLinks.catalog();
- if ((ref5 = g.BOARD.ID) === 'b' || ref5 === 'trash' || ref5 === 'bant') {
+ if (!BoardConfig.isArchived(g.BOARD.ID)) {
$('.archlistlink', this.navLinks).hidden = true;
}
$.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront);
@@ -10339,9 +10513,9 @@ Index = (function() {
$.on(this.selectSort, 'change', this.cb.sort);
$.on(this.selectSize, 'change', $.cb.value);
$.on(this.selectSize, 'change', this.cb.size);
- ref6 = [this.selectMode, this.selectSize];
- for (k = 0, len1 = ref6.length; k < len1; k++) {
- select = ref6[k];
+ ref5 = [this.selectMode, this.selectSize];
+ for (k = 0, len1 = ref5.length; k < len1; k++) {
+ select = ref5[k];
select.value = Conf[select.name];
}
this.selectRev.checked = /-rev$/.test(Index.currentSort);
@@ -10350,12 +10524,12 @@ Index = (function() {
this.lastLongInputs = $$('input', this.lastLongOptions);
this.lastLongThresholds = [0, 0];
this.lastLongOptions.hidden = this.selectSort.value !== 'lastlong';
- ref7 = this.lastLongInputs;
- for (i = l = 0, len2 = ref7.length; l < len2; i = ++l) {
- input = ref7[i];
+ ref6 = this.lastLongInputs;
+ for (i = l = 0, len2 = ref6.length; l < len2; i = ++l) {
+ input = ref6[i];
$.on(input, 'change', this.cb.lastLongThresholds);
tRaw = Conf["Last Long Reply Thresholds " + i];
- input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref8 = tRaw[g.BOARD.ID]) != null ? ref8 : 100 : tRaw;
+ input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref7 = tRaw[g.BOARD.ID]) != null ? ref7 : 100 : tRaw;
}
this.root = $.el('div', {
className: 'board json-index'
@@ -10376,7 +10550,7 @@ Index = (function() {
return d.title = d.title.replace(/\ -\ Page\ \d+/, '');
});
$.onExists(doc, '.board > .thread > .postContainer, .board + *', function() {
- var board, el, len3, m, ref9, timeEl, topNavPos;
+ var board, el, len3, m, ref8, timeEl, topNavPos;
Build.hat = $('.board > .thread > img:first-child');
if (Build.hat) {
g.BOARD.threads.forEach(function(thread) {
@@ -10395,9 +10569,9 @@ Index = (function() {
try {
d.implementation.createDocument(null, null, null).appendChild(board);
} catch (_error) {}
- ref9 = $$('.navLinks');
- for (m = 0, len3 = ref9.length; m < len3; m++) {
- el = ref9[m];
+ ref8 = $$('.navLinks');
+ for (m = 0, len3 = ref8.length; m < len3; m++) {
+ el = ref8[m];
$.rm(el);
}
$.rm($.id('ctrl-top'));
@@ -10969,56 +11143,42 @@ Index = (function() {
return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? '1 hidden thread' : hiddenCount + " hidden threads";
},
update: function(firstTime) {
- var now, ref, ref1;
- if ((ref = Index.req) != null) {
- ref.abort();
+ var oldReq;
+ if ((oldReq = Index.req)) {
+ delete Index.req;
+ oldReq.abort();
}
- if ((ref1 = Index.notice) != null) {
- ref1.close();
- }
- if (Conf['Index Refresh Notifications'] && d.readyState !== 'loading') {
- Index.notice = new Notice('info', 'Refreshing index...');
+ if (Conf['Index Refresh Notifications']) {
+ Index.notice || (Index.notice = new Notice('info', 'Refreshing index...'));
} else {
- now = Date.now();
- $.ready(function() {
- return Index.nTimeout = setTimeout((function() {
- if (Index.req && !Index.notice) {
- return Index.notice = new Notice('info', 'Refreshing index...');
- }
- }), 3 * $.SECOND - (Date.now() - now));
- });
+ Index.nTimeout || (Index.nTimeout = setTimeout(function() {
+ return Index.notice || (Index.notice = new Notice('info', 'Refreshing index...'));
+ }, 3 * $.SECOND));
}
if (!firstTime && d.readyState !== 'loading' && !$('.board + *')) {
location.reload();
return;
}
- Index.req = $.ajax(location.protocol + "//a.4cdn.org/" + g.BOARD + "/catalog.json", {
- onabort: Index.load,
- onloadend: Index.load
- }, {
- whenModified: 'Index'
- });
+ Index.req = $.whenModified(Site.urls.catalogJSON({
+ boardID: g.BOARD.ID
+ }), 'Index', Index.load);
return $.addClass(Index.button, 'fa-spin');
},
- load: function(e) {
- var err, nTimeout, notice, ref, req, timeEl;
+ load: function() {
+ var err, nTimeout, notice, ref, timeEl;
+ if (this !== Index.req) {
+ return;
+ }
$.rmClass(Index.button, 'fa-spin');
- req = Index.req, notice = Index.notice, nTimeout = Index.nTimeout;
+ notice = Index.notice, nTimeout = Index.nTimeout;
if (nTimeout) {
clearTimeout(nTimeout);
}
delete Index.nTimeout;
delete Index.req;
delete Index.notice;
- if (e.type === 'abort') {
- req.onloadend = null;
- if (notice != null) {
- notice.close();
- }
- return;
- }
- if ((ref = req.status) !== 200 && ref !== 304) {
- err = "Index refresh failed. " + (req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error');
+ if ((ref = this.status) !== 200 && ref !== 304) {
+ err = "Index refresh failed. " + (this.status ? "Error " + this.statusText + " (" + this.status + ")" : 'Connection Error');
if (notice) {
notice.setType('warning');
notice.el.lastElementChild.textContent = err;
@@ -11029,9 +11189,9 @@ Index = (function() {
return;
}
try {
- if (req.status === 200) {
- Index.parse(req.response);
- } else if (req.status === 304) {
+ if (this.status === 200) {
+ Index.parse(this.response);
+ } else if (this.status === 304) {
Index.pageLoad();
}
} catch (_error) {
@@ -11056,7 +11216,7 @@ Index = (function() {
}
}
timeEl = $('#index-last-refresh time', Index.navLinks);
- timeEl.dataset.utc = Date.parse(req.getResponseHeader('Last-Modified'));
+ timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified'));
return RelativeDates.update(timeEl);
},
parse: function(pages) {
@@ -11909,20 +12069,27 @@ Settings = (function() {
return $.after($('input[name="Stubs"]', section).parentNode.parentNode, div);
},
"export": function() {
- return $.get(Conf, function(Conf) {
- delete Conf['boardConfig'];
+ var Conf2;
+ Conf2 = {};
+ $.extend(Conf2, Conf);
+ return $.get(Conf2, function(Conf2) {
+ delete Conf2['boardConfig'];
return Settings.downloadExport({
version: g.VERSION,
date: Date.now(),
- Conf: Conf
+ Conf: Conf2
});
});
},
downloadExport: function(data) {
- var a, p;
+ var a, blob, p, url;
+ blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: 'application/json'
+ });
+ url = URL.createObjectURL(blob);
a = $.el('a', {
download: "4chan X v" + g.VERSION + "-" + data.date + ".json",
- href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2)))))
+ href: url
});
p = $('.imp-exp-result', Settings.dialog);
$.rmAll(p);
@@ -12352,6 +12519,11 @@ Settings = (function() {
set('siteProperties', siteProperties);
}
}
+ if (compareString < '00001.00014.00006.00006') {
+ if (data['sauces'] != null) {
+ set('sauces', data['sauces'].replace(/\/\/%\$1\.deviantart\.com\/gallery\/#\/d%\$2;regexp:\/\^\\w\+_by_\(\\w\+\)-d\(\[\\da-z\]\+\)\//g, '//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/'));
+ }
+ }
return changes;
},
loadSettings: function(data, cb) {
@@ -12381,7 +12553,7 @@ Settings = (function() {
filter: function(section) {
var select;
$.extend(section, {
- innerHTML: "Guide General Post number Name Unique ID Tripcode Capcode Pass Date Subject Comment Flag Filename Image dimensions Filesize Image MD5
"
+ innerHTML: "Guide General Post number Name Unique ID Tripcode Capcode Pass Date Email Subject Comment Flag Filename Image dimensions Filesize Image MD5
"
});
select = $('select', section);
$.on(select, 'change', Settings.selectFilter);
@@ -12412,7 +12584,7 @@ Settings = (function() {
};
});
$.extend(div, {
- innerHTML: "Filter is disabled.
Use regular expressions , one per line. Lines starting with a # will be ignored. For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive. MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:Per boards, separate them with commas. It is global if not specified. Use sfw and nsfw to reference all worksafe or not-worksafe boards. For example: boards:a,jp;. In case of a global rule or one that uses sfw/nsfw, select boards to be excluded from the filter. For example: exclude:vg,v;. Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default). For example: op:only;, op:no; or op:yes;. Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`). For example: stub:yes; or stub:no;. Highlight instead of hiding. You can specify a class name to use with a userstyle. For example: highlight; or highlight:wallpaper;. Highlighted OPs will have their threads put on top of the board index by default. For example: top:yes; or top:no;. Filters in the \"General\" section apply to multiple fields, by default subject,name,filename,comment. The fields can be specified with the type option, separated by commas. For example: type:" + E.cat(filterTypes) + ";. Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there. The native catalog has its own separate filter list.
"
+ innerHTML: "Filter is disabled.
Use regular expressions , one per line. Lines starting with a # will be ignored. For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive. MD5 and Unique ID filtering use exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:Per boards, separate them with commas. It is global if not specified. Use sfw and nsfw to reference all worksafe or not-worksafe boards. For example: boards:a,jp;. To specify boards on a particular site, put the beginning of the domain and a slash character before the list. Any initial www. should not be included, and all 4chan domains are considered 4chan.org. For example: boards:4:a,jp,sama:a,z;. An asterisk can be used to specify all boards on a site. For example: boards:4:*;. Select boards to be excluded from the filter. The syntax is the same as for the boards: option above. For example: exclude:vg,v;. Filter OPs only along with their threads (\`only\`) or replies only (\`no\`). For example: op:only; or op:no;. Filter only posts with files (\`only\`) or only posts without files (\`no\`). For example: file:only; or file:no;. Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`). For example: stub:yes; or stub:no;. Highlight instead of hiding. You can specify a class name to use with a userstyle. For example: highlight; or highlight:wallpaper;. Highlighted OPs will have their threads put on top of the board index by default. For example: top:yes; or top:no;. Show a desktop notification instead of hiding. For example: notify;. Filters in the \"General\" section apply to multiple fields, by default subject,name,filename,comment. The fields can be specified with the type option, separated by commas. For example: type:" + E.cat(filterTypes) + ";. Types can also be combined with a + sign; this indicates the filter applies to the given fields joined by newlines. For example: type:filename+filesize+dimensions;. Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there. The native catalog has its own separate filter list.
"
});
return $('.warning', div).hidden = Conf['Filter'];
},
@@ -13119,8 +13291,9 @@ UI = (function() {
};
hoverstart = function(arg) {
- var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
- root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
+ var cb, el, endEvents, height, latestEvent, noRemove, o, rect, ref, root, width;
+ root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, width = arg.width, cb = arg.cb, noRemove = arg.noRemove;
+ rect = root.getBoundingClientRect();
o = {
root: root,
el: el,
@@ -13132,7 +13305,10 @@ UI = (function() {
clientHeight: doc.clientHeight,
clientWidth: doc.clientWidth,
height: height,
- noRemove: noRemove
+ width: width,
+ noRemove: noRemove,
+ clientX: (rect.left + rect.right) / 2,
+ clientY: (rect.top + rect.bottom) / 2
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
@@ -13160,16 +13336,22 @@ UI = (function() {
hoverstart.padding = 25;
hover = function(e) {
- var clientX, clientY, height, left, ref, right, style, threshold, top;
+ var clientX, clientY, height, left, marginX, ref, ref1, right, style, threshold, top, width;
this.latestEvent = e;
height = (this.height || this.el.offsetHeight) + hoverstart.padding;
- clientX = e.clientX, clientY = e.clientY;
+ width = this.width || this.el.offsetWidth;
+ ref = Conf['Follow Cursor'] ? e : this, clientX = ref.clientX, clientY = ref.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
if (!this.isImage) {
threshold = Math.max(threshold, this.clientWidth - 400);
}
- ref = clientX <= threshold ? [clientX + 45 + 'px', ''] : ['', this.clientWidth - clientX + 45 + 'px'], left = ref[0], right = ref[1];
+ marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45;
+ if (this.isImage) {
+ marginX = Math.min(marginX, this.clientWidth - width);
+ }
+ marginX += 'px';
+ ref1 = clientX <= threshold ? [marginX, ''] : ['', marginX], left = ref1[0], right = ref1[1];
style = this.style;
style.top = top + 'px';
style.left = left;
@@ -13878,8 +14060,11 @@ ImageCommon = (function() {
return cb(URL);
}
};
- return $.ajax(location.protocol + "//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", {
- onload: function() {
+ return $.ajax(Site.urls.threadJSON({
+ boardID: post.boardID,
+ threadID: post.threadID
+ }), {
+ onloadend: function() {
var i, len, postObj, ref;
if (this.status === 404) {
post.kill(!post.isClone);
@@ -14272,7 +14457,7 @@ ImageExpand = (function() {
}
},
mouseout: function(e) {
- if (mousedown && e.clientX <= this.getBoundingClientRect().left) {
+ if (((e.buttons & 1) || mousedown) && e.clientX <= this.getBoundingClientRect().left) {
return ImageExpand.toggle(Get.postFromNode(this));
}
}
@@ -14453,7 +14638,7 @@ ImageHover = (function() {
},
mouseover: function(post) {
return function(e) {
- var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
+ var el, error, file, height, isVideo, maxHeight, maxWidth, ref, ref1, scale, width, x;
if (!doc.contains(this)) {
return;
}
@@ -14489,28 +14674,32 @@ ImageHover = (function() {
}
}
}
- ref1 = (function() {
- var i, len, ref1, results;
- ref1 = file.dimensions.split('x');
- results = [];
- for (i = 0, len = ref1.length; i < len; i++) {
- x = ref1[i];
- results.push(+x);
- }
- return results;
- })(), width = ref1[0], height = ref1[1];
- ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
- maxWidth = Math.max(left, doc.clientWidth - right);
- maxHeight = doc.clientHeight - UI.hover.padding;
- scale = Math.min(1, maxWidth / width, maxHeight / height);
- el.style.maxWidth = (scale * width) + "px";
- el.style.maxHeight = (scale * height) + "px";
+ if (file.dimensions) {
+ ref1 = (function() {
+ var i, len, ref1, results;
+ ref1 = file.dimensions.split('x');
+ results = [];
+ for (i = 0, len = ref1.length; i < len; i++) {
+ x = ref1[i];
+ results.push(+x);
+ }
+ return results;
+ })(), width = ref1[0], height = ref1[1];
+ maxWidth = doc.clientWidth;
+ maxHeight = doc.clientHeight - UI.hover.padding;
+ scale = Math.min(1, maxWidth / width, maxHeight / height);
+ width *= scale;
+ height *= scale;
+ el.style.maxWidth = width + "px";
+ el.style.maxHeight = height + "px";
+ }
return UI.hover({
root: this,
el: el,
latestEvent: e,
endEvents: 'mouseout click',
- height: scale * height,
+ height: height,
+ width: width,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@@ -14918,7 +15107,7 @@ Sauce = (function() {
if (!matches) {
return orig;
}
- type = matches[parameter.slice(1)];
+ type = matches[parameter.slice(1)] || '';
} else {
type = Sauce.formatters[parameter](post, ext);
if (type == null) {
@@ -15368,7 +15557,7 @@ Embedding = (function() {
return Embedding.flushTitles(service);
}
} else {
- return CrossOrigin.json(service.api(uid), (function() {
+ return CrossOrigin.cache(service.api(uid), (function() {
return Embedding.cb.title(this, data);
}));
}
@@ -15387,7 +15576,7 @@ Embedding = (function() {
Embedding.cb.title(this, data);
}
};
- return CrossOrigin.json(service.api((function() {
+ return CrossOrigin.cache(service.api((function() {
var j, len, results;
results = [];
for (j = 0, len = queue.length; j < len; j++) {
@@ -15609,7 +15798,7 @@ Embedding = (function() {
hidden: true,
id: "gist-embed-" + (counter++)
});
- CrossOrigin.json("https://api.github.com/gists/" + a.dataset.uid, function() {
+ CrossOrigin.cache("https://api.github.com/gists/" + a.dataset.uid, function() {
el.textContent = Object.values(this.response.files)[0].content;
el.className = 'prettyprint';
$.global(function() {
@@ -16165,7 +16354,7 @@ ArchiveLink = (function() {
} : function(post) {
var typeParam, value;
typeParam = type === 'country' && post.info.flagCodeTroll ? 'tag' : type;
- value = type === 'country' ? post.info.flagCode || post.info.flagCodeTroll : Filter[type](post);
+ value = type === 'country' ? post.info.flagCode || post.info.flagCodeTroll : Filter.value(type, post);
if (!value) {
return false;
}
@@ -16340,18 +16529,21 @@ DeleteLink = (function() {
return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), {
responseType: 'document',
withCredentials: true,
- onload: function() {
+ onloadend: function() {
return DeleteLink.load(link, post, fileOnly, this.response);
},
- onerror: function() {
- return DeleteLink.error(link, post);
- }
- }, {
form: $.formData(form)
});
},
load: function(link, post, fileOnly, resDoc) {
var el, msg;
+ if (!resDoc) {
+ new Notice('warning', 'Connection error, please retry.', 20);
+ if (post.fullID === DeleteLink.post.fullID) {
+ $.on(link, 'click', DeleteLink.toggle);
+ }
+ return;
+ }
link.textContent = DeleteLink.linkText(fileOnly);
if (resDoc.title === '4chan - Banned') {
el = $.el('span', {
@@ -16380,12 +16572,6 @@ DeleteLink = (function() {
}
}
},
- error: function(link, post) {
- new Notice('warning', 'Connection error, please retry.', 20);
- if (post.fullID === DeleteLink.post.fullID) {
- return $.on(link, 'click', DeleteLink.toggle);
- }
- },
cooldown: {
seconds: {},
start: function(post, seconds) {
@@ -16880,7 +17066,7 @@ CatalogLinks = (function() {
if (board == null) {
board = g.BOARD.ID;
}
- if (Conf['External Catalog'] && (board === 'a' || board === 'c' || board === 'g' || board === 'biz' || board === 'k' || board === 'm' || board === 'o' || board === 'p' || board === 'v' || board === 'vg' || board === 'vr' || board === 'w' || board === 'wg' || board === 'cm' || board === '3' || board === 'adv' || board === 'an' || board === 'asp' || board === 'cgl' || board === 'ck' || board === 'co' || board === 'diy' || board === 'fa' || board === 'fit' || board === 'gd' || board === 'int' || board === 'jp' || board === 'lit' || board === 'mlp' || board === 'mu' || board === 'n' || board === 'out' || board === 'po' || board === 'sci' || board === 'sp' || board === 'tg' || board === 'toy' || board === 'trv' || board === 'tv' || board === 'vp' || board === 'wsg' || board === 'x' || board === 'f' || board === 'pol' || board === 's4s' || board === 'lgbt')) {
+ if (Conf['External Catalog'] && (board === '3' || board === 'a' || board === 'adv' || board === 'an' || board === 'asp' || board === 'biz' || board === 'c' || board === 'cgl' || board === 'ck' || board === 'cm' || board === 'co' || board === 'diy' || board === 'f' || board === 'fa' || board === 'fit' || board === 'g' || board === 'gd' || board === 'his' || board === 'i' || board === 'int' || board === 'jp' || board === 'k' || board === 'lgbt' || board === 'lit' || board === 'm' || board === 'mlp' || board === 'mu' || board === 'n' || board === 'news' || board === 'o' || board === 'out' || board === 'p' || board === 'po' || board === 'pol' || board === 's4s' || board === 'sci' || board === 'sp' || board === 'tg' || board === 'toy' || board === 'trv' || board === 'tv' || board === 'v' || board === 'vg' || board === 'vip' || board === 'vp' || board === 'vr' || board === 'w' || board === 'wg' || board === 'wsg' || board === 'wsr' || board === 'x')) {
return "//catalog.neet.tv/" + board + "/";
} else if (Conf['JSON Index'] && Conf['Use 4chan X Catalog']) {
if (((ref = location.hostname) === 'boards.4chan.org' || ref === 'boards.4channel.org') && g.BOARD.ID === board && g.VIEW === 'index') {
@@ -16952,12 +17138,6 @@ ExpandComment = (function() {
if (g.VIEW !== 'index' || !Conf['Comment Expansion'] || Conf['JSON Index']) {
return;
}
- if (g.BOARD.ID === 'g') {
- this.callbacks.push(Fourchan.code);
- }
- if (g.BOARD.ID === 'sci') {
- this.callbacks.push(Fourchan.math);
- }
return Callbacks.Post.push({
name: 'Comment Expansion',
cb: this.node
@@ -16985,7 +17165,10 @@ ExpandComment = (function() {
return;
}
a.textContent = "Post No." + post + " Loading...";
- return $.cache(location.protocol + "//a.4cdn.org" + (a.pathname.split(/\/+/).splice(0, 4).join('/')) + ".json", function() {
+ return $.cache(Site.urls.threadJSON({
+ boardID: post.boardID,
+ threadID: post.threadID
+ }), function() {
return ExpandComment.parse(this, a, post);
});
},
@@ -17003,7 +17186,7 @@ ExpandComment = (function() {
var callback, clone, comment, href, i, j, k, len, len1, len2, postObj, posts, quote, ref, ref1, spoilerRange, status;
status = req.status;
if (status !== 200 && status !== 304) {
- a.textContent = "Error " + req.statusText + " (" + status + ")";
+ a.textContent = status ? "Error " + req.statusText + " (" + status + ")" : 'Connection Error';
return;
}
posts = req.response.posts;
@@ -17084,15 +17267,16 @@ ExpandThread = (function() {
return $.on(a, 'click', ExpandThread.cbToggle);
},
disconnect: function(refresh) {
- var ref, ref1, status, threadID;
+ var oldReq, ref, status, threadID;
if (g.VIEW === 'thread' || !Conf['Thread Expansion']) {
return;
}
ref = ExpandThread.statuses;
for (threadID in ref) {
status = ref[threadID];
- if ((ref1 = status.req) != null) {
- ref1.abort();
+ if ((oldReq = status.req)) {
+ delete status.req;
+ oldReq.abort();
}
delete ExpandThread.statuses[threadID];
}
@@ -17140,17 +17324,24 @@ ExpandThread = (function() {
var status;
ExpandThread.statuses[thread] = status = {};
a.textContent = Build.summaryText.apply(Build, ['...'].concat(slice.call(a.textContent.match(/\d+/g))));
- return status.req = $.cache(location.protocol + "//a.4cdn.org/" + thread.board + "/thread/" + thread + ".json", function() {
+ return status.req = $.cache(Site.urls.threadJSON({
+ boardID: thread.board.ID,
+ threadID: thread.ID
+ }), function() {
+ if (this !== status.req) {
+ return;
+ }
delete status.req;
return ExpandThread.parse(this, thread, a);
});
},
contract: function(thread, a, threadRoot) {
- var filesCount, i, inlined, len, num, postsCount, replies, reply, status;
+ var filesCount, i, inlined, len, num, oldReq, postsCount, replies, reply, status;
status = ExpandThread.statuses[thread];
delete ExpandThread.statuses[thread];
- if (status.req) {
- status.req.abort();
+ if ((oldReq = status.req)) {
+ delete status.req;
+ oldReq.abort();
if (a) {
a.textContent = Build.summaryText.apply(Build, ['+'].concat(slice.call(a.textContent.match(/\d+/g))));
}
@@ -17200,7 +17391,7 @@ ExpandThread = (function() {
parse: function(req, thread, a) {
var a2, filesCount, i, len, post, postData, posts, postsCount, postsRoot, ref, ref1, root;
if ((ref = req.status) !== 200 && ref !== 304) {
- a.textContent = "Error " + req.statusText + " (" + req.status + ")";
+ a.textContent = req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error';
return;
}
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler;
@@ -17449,10 +17640,14 @@ Fourchan = (function() {
Fourchan = {
init: function() {
var ref;
- if ((ref = g.VIEW) !== 'index' && ref !== 'thread' && ref !== 'archive') {
+ if (!(Site.software === 'yotsuba' && ((ref = g.VIEW) === 'index' || ref === 'thread' || ref === 'archive'))) {
return;
}
- if (g.BOARD.ID === 'g') {
+ BoardConfig.ready(this.initBoard);
+ return Main.ready(this.initReady);
+ },
+ initBoard: function() {
+ if (g.BOARD.config.code_tags) {
$.on(window, 'prettyprint:cb', function(e) {
var post, pre;
if (!(post = g.posts[e.detail.ID])) {
@@ -17468,11 +17663,15 @@ Fourchan = (function() {
});
$.globalEval('window.addEventListener(\'prettyprint\', function(e) {\n window.dispatchEvent(new CustomEvent(\'prettyprint:cb\', {\n detail: {\n ID: e.detail.ID,\n i: e.detail.i,\n html: prettyPrintOne(e.detail.html)\n }\n }));\n}, false);');
Callbacks.Post.push({
- name: 'Parse /g/ code',
- cb: this.code
+ name: 'Parse [code] tags',
+ cb: Fourchan.code
});
+ g.posts.forEach(function(post) {
+ return Callbacks.Post.execute(post, ['Parse [code] tags'], true);
+ });
+ ExpandComment.callbacks.push(Fourchan.code);
}
- if (g.BOARD.ID === 'sci') {
+ if (g.BOARD.config.math_tags) {
$.global(function() {
return window.addEventListener('mathjax', function(e) {
if (window.MathJax) {
@@ -17491,20 +17690,24 @@ Fourchan = (function() {
}, false);
});
Callbacks.Post.push({
- name: 'Parse /sci/ math',
- cb: this.math
+ name: 'Parse [math] tags',
+ cb: Fourchan.math
});
+ g.posts.forEach(function(post) {
+ return Callbacks.Post.execute(post, ['Parse [math] tags'], true);
+ });
+ return ExpandComment.callbacks.push(Fourchan.math);
}
- return Main.ready(function() {
- return $.global(function() {
- var j, len, node, ref1;
- window.clickable_ids = false;
- ref1 = document.querySelectorAll('.posteruid, .capcode');
- for (j = 0, len = ref1.length; j < len; j++) {
- node = ref1[j];
- node.removeEventListener('click', window.idClick, false);
- }
- });
+ },
+ initReady: function() {
+ return $.global(function() {
+ var j, len, node, ref;
+ window.clickable_ids = false;
+ ref = document.querySelectorAll('.posteruid, .capcode');
+ for (j = 0, len = ref.length; j < len; j++) {
+ node = ref[j];
+ node.removeEventListener('click', window.idClick, false);
+ }
});
},
code: function() {
@@ -17881,6 +18084,12 @@ Keybinds = (function() {
}
ThreadWatcher.toggleWatcher();
break;
+ case Conf['Toggle threading']:
+ if (!QuoteThreading.ready) {
+ return;
+ }
+ QuoteThreading.toggleThreading();
+ break;
case Conf['Mark thread read']:
if (!(g.VIEW === 'index' && thread && UnreadIndex.enabled)) {
return;
@@ -18735,7 +18944,6 @@ Report = (function() {
results = [];
fn = function(name, url) {
return $.ajax(url, {
- responseType: 'json',
onloadend: function() {
results.push([
name, this.response || {
@@ -18745,8 +18953,7 @@ Report = (function() {
if (results.length === urls.length) {
return cb(results);
}
- }
- }, {
+ },
form: form
});
};
@@ -19378,12 +19585,9 @@ ThreadStats = (function() {
return;
}
ThreadStats.timeout = setTimeout(ThreadStats.fetchPage, 2 * $.MINUTE);
- return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadStats.thread.board + "/threads.json", {
- onload: ThreadStats.onThreadsLoad
- }, {
- whenModified: 'ThreadStats',
- bypassCache: true
- });
+ return $.whenModified(Site.urls.threadsListJSON({
+ boardID: ThreadStats.thread.board
+ }), 'ThreadStats', ThreadStats.onThreadsLoad);
},
onThreadsLoad: function() {
var i, j, k, len, len1, len2, page, purgePos, ref, ref1, ref2, thread;
@@ -19592,11 +19796,12 @@ ThreadUpdater = (function() {
}
},
load: function() {
- var req;
- req = ThreadUpdater.req;
- switch (req.status) {
+ if (this !== ThreadUpdater.req) {
+ return;
+ }
+ switch (this.status) {
case 200:
- ThreadUpdater.parse(req);
+ ThreadUpdater.parse(this);
if (ThreadUpdater.thread.isArchived) {
return ThreadUpdater.kill();
} else {
@@ -19604,7 +19809,9 @@ ThreadUpdater = (function() {
}
break;
case 404:
- return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", {
+ return $.ajax(Site.urls.catalogJSON({
+ boardID: ThreadUpdater.thread.board.ID
+ }), {
onloadend: function() {
var confirmed, i, k, len, len1, page, ref, ref1, thread;
if (this.status === 200) {
@@ -19627,12 +19834,12 @@ ThreadUpdater = (function() {
if (confirmed) {
return ThreadUpdater.kill();
} else {
- return ThreadUpdater.error(req);
+ return ThreadUpdater.error(this);
}
}
});
default:
- return ThreadUpdater.error(req);
+ return ThreadUpdater.error(this);
}
}
},
@@ -19710,18 +19917,18 @@ ThreadUpdater = (function() {
return ThreadUpdater.seconds--;
},
update: function() {
- var ref;
+ var oldReq;
clearTimeout(ThreadUpdater.timeoutID);
ThreadUpdater.set('timer', '...', 'loading');
- if ((ref = ThreadUpdater.req) != null) {
- ref.abort();
+ if ((oldReq = ThreadUpdater.req)) {
+ delete ThreadUpdater.req;
+ oldReq.abort();
}
- return ThreadUpdater.req = $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/thread/" + ThreadUpdater.thread + ".json", {
- onloadend: ThreadUpdater.cb.load,
+ return ThreadUpdater.req = $.whenModified(Site.urls.threadJSON({
+ boardID: ThreadUpdater.thread.board.ID,
+ threadID: ThreadUpdater.thread.ID
+ }), 'ThreadUpdater', ThreadUpdater.cb.load, {
timeout: $.MINUTE
- }, {
- whenModified: 'ThreadUpdater',
- bypassCache: true
});
},
updateThreadStatus: function(type, status) {
@@ -19881,6 +20088,7 @@ ThreadWatcher = (function() {
className: 'fa fa-eye'
});
this.db = new DataBoard('watchedThreads', this.refresh, true);
+ this.dbLM = new DataBoard('watcherLastModified', null, true);
this.dialog = UI.dialog('thread-watcher', {
innerHTML: "
"
});
@@ -19911,6 +20119,7 @@ ThreadWatcher = (function() {
this.dialog.hidden = true;
}
Header.addShortcut('watcher', sc, 510);
+ ThreadWatcher.initLastModified();
ThreadWatcher.fetchAuto();
$.on(window, 'visibilitychange focus', function() {
return $.queueTask(ThreadWatcher.fetchAuto);
@@ -19974,7 +20183,7 @@ ThreadWatcher = (function() {
return toggler.title = (isWatched ? 'Unwatch' : 'Watch') + " Thread";
},
node: function() {
- var boardID, data, threadID, toggler;
+ var boardID, data, siteID, threadID, toggler;
if (this.isReply) {
return;
}
@@ -19987,9 +20196,11 @@ ThreadWatcher = (function() {
});
$.before($('input', this.nodes.info), toggler);
}
+ siteID = Site.hostname;
boardID = this.board.ID;
threadID = this.thread.ID;
data = ThreadWatcher.db.get({
+ siteID: siteID,
boardID: boardID,
threadID: threadID
});
@@ -19998,14 +20209,11 @@ ThreadWatcher = (function() {
if (data && (data.excerpt == null)) {
return $.queueTask((function(_this) {
return function() {
- ThreadWatcher.db.extend({
- boardID: boardID,
- threadID: threadID,
+ return ThreadWatcher.update(siteID, boardID, threadID, {
val: {
excerpt: Get.threadExcerpt(_this.thread)
}
});
- return ThreadWatcher.refresh();
};
})(this));
}
@@ -20039,25 +20247,25 @@ ThreadWatcher = (function() {
},
cb: {
openAll: function() {
- var a, i, len1, ref;
+ var a, j, len1, ref;
if ($.hasClass(this, 'disabled')) {
return;
}
ref = $$('a[title]', ThreadWatcher.list);
- for (i = 0, len1 = ref.length; i < len1; i++) {
- a = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ a = ref[j];
$.open(a.href);
}
return $.event('CloseMenu');
},
pruneDeads: function() {
- var boardID, data, i, len1, ref, ref1, siteID, threadID;
+ var boardID, data, j, len1, ref, ref1, siteID, threadID;
if ($.hasClass(this, 'disabled')) {
return;
}
ref = ThreadWatcher.getAll();
- for (i = 0, len1 = ref.length; i < len1; i++) {
- ref1 = ref[i], siteID = ref1.siteID, boardID = ref1.boardID, threadID = ref1.threadID, data = ref1.data;
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ ref1 = ref[j], siteID = ref1.siteID, boardID = ref1.boardID, threadID = ref1.threadID, data = ref1.data;
if (data.isDead) {
ThreadWatcher.db["delete"]({
siteID: siteID,
@@ -20108,28 +20316,32 @@ ThreadWatcher = (function() {
})) {
continue;
}
- nKilled++;
if (Conf['Auto Prune'] || !(data && typeof data === 'object')) {
db["delete"]({
boardID: boardID,
threadID: threadID
});
+ nKilled++;
+ } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ ThreadWatcher.fetchStatus({
+ siteID: siteID,
+ boardID: boardID,
+ threadID: threadID,
+ data: data
+ });
} else {
db.extend({
boardID: boardID,
threadID: threadID,
val: {
- isDead: true
+ isDead: true,
+ page: void 0,
+ lastPage: void 0,
+ unread: void 0,
+ quotingYou: void 0
}
});
- if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
- ThreadWatcher.fetchStatus({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- data: data
- });
- }
+ nKilled++;
}
}
if (nKilled) {
@@ -20147,6 +20359,38 @@ ThreadWatcher = (function() {
},
requests: [],
fetched: 0,
+ fetch: function(url, arg, args, cb) {
+ var ajax, force, onloadend, ref, req, siteID;
+ siteID = arg.siteID, force = arg.force;
+ if (ThreadWatcher.requests.length === 0) {
+ ThreadWatcher.status.textContent = '...';
+ $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
+ }
+ onloadend = function() {
+ if (this.finished) {
+ return;
+ }
+ this.finished = true;
+ ThreadWatcher.fetched++;
+ if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
+ ThreadWatcher.clearRequests();
+ } else {
+ ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%";
+ }
+ return cb.apply(this, args);
+ };
+ ajax = siteID === Site.hostname ? $.ajax : CrossOrigin.ajax;
+ if (force) {
+ if ((ref = $.lastModified.ThreadWatcher) != null) {
+ delete ref[url];
+ }
+ }
+ req = $.whenModified(url, 'ThreadWatcher', onloadend, {
+ timeout: $.MINUTE,
+ ajax: ajax
+ });
+ return ThreadWatcher.requests.push(req);
+ },
clearRequests: function() {
ThreadWatcher.requests = [];
ThreadWatcher.fetched = 0;
@@ -20154,16 +20398,45 @@ ThreadWatcher = (function() {
return $.rmClass(ThreadWatcher.refreshButton, 'fa-spin');
},
abort: function() {
- var i, len1, ref, req;
+ var j, len1, ref, req;
+ delete ThreadWatcher.syncing;
ref = ThreadWatcher.requests;
- for (i = 0, len1 = ref.length; i < len1; i++) {
- req = ref[i];
- if (req.readyState !== 4) {
- req.abort();
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ req = ref[j];
+ if (!(!req.finished)) {
+ continue;
}
+ req.finished = true;
+ req.abort();
}
return ThreadWatcher.clearRequests();
},
+ initLastModified: function() {
+ var base, boardID, boards, data, date, lm, ref, ref1, siteID, url;
+ lm = ((base = $.lastModified)['ThreadWatcher'] || (base['ThreadWatcher'] = {}));
+ ref = ThreadWatcher.dbLM.data;
+ for (siteID in ref) {
+ boards = ref[siteID];
+ ref1 = boards.boards;
+ for (boardID in ref1) {
+ data = ref1[boardID];
+ if (ThreadWatcher.db.get({
+ siteID: siteID,
+ boardID: boardID
+ })) {
+ for (url in data) {
+ date = data[url];
+ lm[url] = date;
+ }
+ } else {
+ ThreadWatcher.dbLM["delete"]({
+ siteID: siteID,
+ boardID: boardID
+ });
+ }
+ }
+ }
+ },
fetchAuto: function() {
var db, interval, now, ref;
clearTimeout(ThreadWatcher.timeout);
@@ -20171,46 +20444,169 @@ ThreadWatcher = (function() {
return;
}
db = ThreadWatcher.db;
- interval = ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] ? 5 * $.MINUTE : 2 * $.HOUR;
+ interval = Conf['Show Page'] || (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) ? 5 * $.MINUTE : 2 * $.HOUR;
now = Date.now();
if (!((now - interval < (ref = db.data.lastChecked || 0) && ref <= now) || d.hidden || !d.hasFocus())) {
ThreadWatcher.fetchAllStatus();
- db.setLastChecked();
}
return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval);
},
buttonFetchAll: function() {
- if (ThreadWatcher.requests.length) {
+ if (ThreadWatcher.syncing || ThreadWatcher.requests.length) {
return ThreadWatcher.abort();
} else {
return ThreadWatcher.fetchAllStatus();
}
},
fetchAllStatus: function() {
- var db, dbs, i, len1, n, results;
+ var dbi, dbs, j, len1, n, results;
+ ThreadWatcher.status.textContent = '...';
+ $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
+ ThreadWatcher.syncing = true;
dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(function(x) {
return x;
});
n = 0;
results = [];
- for (i = 0, len1 = dbs.length; i < len1; i++) {
- db = dbs[i];
- results.push(db.forceSync(function() {
- var j, len2, thread, threads;
+ for (j = 0, len1 = dbs.length; j < len1; j++) {
+ dbi = dbs[j];
+ results.push(dbi.forceSync(function() {
+ var board, boards, db, deep, k, len2, now, ref;
if ((++n) === dbs.length) {
- threads = ThreadWatcher.getAll();
- for (j = 0, len2 = threads.length; j < len2; j++) {
- thread = threads[j];
- ThreadWatcher.fetchStatus(thread);
+ if (!ThreadWatcher.syncing) {
+ return;
+ }
+ delete ThreadWatcher.syncing;
+ db = ThreadWatcher.db;
+ now = Date.now();
+ deep = !((now - 2 * $.HOUR < (ref = db.data.lastChecked2 || 0) && ref <= now));
+ boards = ThreadWatcher.getAll(true);
+ for (k = 0, len2 = boards.length; k < len2; k++) {
+ board = boards[k];
+ ThreadWatcher.fetchBoard(board, deep);
+ }
+ db.setLastChecked();
+ if (deep) {
+ db.setLastChecked('lastChecked2');
+ }
+ if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
+ return ThreadWatcher.clearRequests();
}
}
}));
}
return results;
},
- fetchStatus: function(thread, force) {
- var base, boardID, data, ref, ref1, req, siteID, software, threadID, url;
- siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data;
+ fetchBoard: function(board, deep) {
+ var base, boardID, force, ref, ref1, ref2, siteID, software, url, urlF;
+ if (!board.some(function(thread) {
+ return !thread.data.isDead;
+ })) {
+ return;
+ }
+ force = Conf['Show Page'] && board.some(function(thread) {
+ return (thread.data.page == null) && !thread.data.isDead && thread.data.last !== -1;
+ });
+ ref = board[0], siteID = ref.siteID, boardID = ref.boardID;
+ software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0;
+ urlF = deep && software === 'tinyboard' ? 'catalogJSON' : 'threadsListJSON';
+ url = (ref2 = SW[software]) != null ? typeof (base = ref2.urls)[urlF] === "function" ? base[urlF]({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0 : void 0;
+ if (!url) {
+ return;
+ }
+ return ThreadWatcher.fetch(url, {
+ siteID: siteID,
+ force: force
+ }, [board, url], ThreadWatcher.parseBoard);
+ },
+ parseBoard: function(board, url) {
+ var boardID, data, i, index, item, j, k, l, lastPage, len1, len2, len3, len4, lmDate, m, modified, nThreads, oldest, page, pageLength, ref, ref1, ref2, ref3, ref4, ref5, ref6, replies, siteID, software, thread, threadID, threads;
+ if (this.status !== 200) {
+ return;
+ }
+ ref = board[0], siteID = ref.siteID, boardID = ref.boardID;
+ software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0;
+ lmDate = this.getResponseHeader('Last-Modified');
+ ThreadWatcher.dbLM.extend({
+ siteID: siteID,
+ boardID: boardID,
+ val: $.item(url, lmDate)
+ });
+ threads = {};
+ pageLength = 0;
+ nThreads = 0;
+ oldest = null;
+ try {
+ pageLength = ((ref2 = this.response[0]) != null ? ref2.threads.length : void 0) || 0;
+ ref3 = this.response;
+ for (i = j = 0, len1 = ref3.length; j < len1; i = ++j) {
+ page = ref3[i];
+ ref4 = page.threads;
+ for (k = 0, len2 = ref4.length; k < len2; k++) {
+ item = ref4[k];
+ threads[item.no] = {
+ page: i + 1,
+ index: nThreads,
+ modified: item.last_modified,
+ replies: item.replies
+ };
+ nThreads++;
+ if ((oldest == null) || item.no < oldest) {
+ oldest = item.no;
+ }
+ }
+ }
+ } catch (_error) {
+ for (l = 0, len3 = board.length; l < len3; l++) {
+ thread = board[l];
+ ThreadWatcher.fetchStatus(thread);
+ }
+ }
+ for (m = 0, len4 = board.length; m < len4; m++) {
+ thread = board[m];
+ threadID = thread.threadID, data = thread.data;
+ if (threads[threadID]) {
+ ref5 = threads[threadID], page = ref5.page, index = ref5.index, modified = ref5.modified, replies = ref5.replies;
+ if (Conf['Show Page']) {
+ lastPage = ((ref6 = SW[software]) != null ? typeof ref6.isPrunedByAge === "function" ? ref6.isPrunedByAge({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0 : void 0) ? threadID === oldest : index >= nThreads - pageLength;
+ ThreadWatcher.update(siteID, boardID, threadID, {
+ page: page,
+ lastPage: lastPage
+ });
+ }
+ if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ if (modified !== data.modified || ((replies != null) && replies !== data.replies)) {
+ ThreadWatcher.db.extend({
+ siteID: siteID,
+ boardID: boardID,
+ threadID: threadID,
+ val: {
+ modified: modified
+ }
+ });
+ ThreadWatcher.fetchStatus(thread);
+ }
+ }
+ } else {
+ if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ ThreadWatcher.fetchStatus(thread);
+ } else {
+ ThreadWatcher.update(siteID, boardID, threadID, {
+ isDead: true
+ });
+ }
+ }
+ }
+ },
+ fetchStatus: function(thread) {
+ var base, boardID, data, force, ref, ref1, siteID, software, threadID, url;
+ siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data, force = thread.force;
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
url = (ref1 = SW[software]) != null ? typeof (base = ref1.urls).threadJSON === "function" ? base.threadJSON({
siteID: siteID,
@@ -20226,54 +20622,21 @@ ThreadWatcher = (function() {
if (data.last === -1) {
return;
}
- if (ThreadWatcher.requests.length === 0) {
- ThreadWatcher.status.textContent = '...';
- $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
- }
- if ((typeof Site.hasCORS === "function" ? Site.hasCORS(url) : void 0) || url.split('/').slice(0, 3).join('/') === location.origin) {
- req = $.ajax(url, {
- onloadend: function() {
- return ThreadWatcher.parseStatus.call(this, thread);
- },
- timeout: $.MINUTE
- }, {
- whenModified: force ? false : 'ThreadWatcher'
- });
- } else {
- req = {
- abort: function() {
- return req.aborted = true;
- }
- };
- CrossOrigin.json(url, function() {
- if (req.aborted) {
- return;
- }
- return ThreadWatcher.parseStatus.call(this, thread);
- }, true, $.MINUTE);
- }
- return ThreadWatcher.requests.push(req);
+ return ThreadWatcher.fetch(url, {
+ siteID: siteID,
+ force: force
+ }, [thread], ThreadWatcher.parseStatus);
},
parseStatus: function(arg) {
- var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, siteID, software, threadID, unread, updated, youOP;
+ var boardID, data, isDead, j, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, replies, siteID, software, threadID, unread, youOP;
siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, data = arg.data;
- ThreadWatcher.fetched++;
- if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
- ThreadWatcher.clearRequests();
- } else {
- ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%";
- }
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
if (this.status === 200 && this.response) {
last = this.response.posts[this.response.posts.length - 1].no;
+ replies = this.response.posts.length - 1;
isDead = !!this.response.posts[0].archived;
if (isDead && Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID
- });
- ThreadWatcher.refresh();
+ ThreadWatcher.rm(siteID, boardID, threadID);
return;
}
if (last === data.last && isDead === data.isDead) {
@@ -20294,8 +20657,8 @@ ThreadWatcher = (function() {
postID: threadID
}) : void 0);
ref2 = this.response.posts;
- for (i = 0, len1 = ref2.length; i < len1; i++) {
- postObj = ref2[i];
+ for (j = 0, len1 = ref2.length; j < len1; j++) {
+ postObj = ref2[j];
if (!(postObj.no > lastReadPost)) {
continue;
}
@@ -20308,7 +20671,7 @@ ThreadWatcher = (function() {
continue;
}
unread++;
- if (!quotingYou && !Conf['Require OP Quote Link'] && youOP && !Filter.isHidden(Build.parseJSON(postObj, boardID))) {
+ if (!quotingYou && !Conf['Require OP Quote Link'] && youOP && !Filter.isHidden(Build.parseJSON(postObj, boardID, siteID))) {
quotingYou = true;
continue;
}
@@ -20329,58 +20692,31 @@ ThreadWatcher = (function() {
break;
}
}
- if (quotesYou && !Filter.isHidden(Build.parseJSON(postObj, boardID))) {
+ if (quotesYou && !Filter.isHidden(Build.parseJSON(postObj, boardID, siteID))) {
quotingYou = true;
}
}
- updated = isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou;
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- last: last,
- isDead: isDead,
- unread: unread,
- quotingYou: quotingYou
- }
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ last: last,
+ replies: replies,
+ isDead: isDead,
+ unread: unread,
+ quotingYou: quotingYou
});
- if (updated) {
- return ThreadWatcher.refresh();
- }
} else if (this.status === 404) {
if (SW[software].mayLackJSON && (data.last == null)) {
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- last: -1
- },
- rm: ['unread', 'quotingYou']
- });
- } else if (Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ last: -1
});
} else {
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- isDead: true
- },
- rm: ['unread', 'quotingYou']
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ isDead: true
});
}
- return ThreadWatcher.refresh();
}
},
- getAll: function() {
- var all, boardID, boards, data, ref, ref1, siteID, threadID, threads;
+ getAll: function(groupByBoard) {
+ var all, boardID, boards, cont, data, ref, ref1, siteID, threadID, threads;
all = [];
ref = ThreadWatcher.db.data;
for (siteID in ref) {
@@ -20391,10 +20727,13 @@ ThreadWatcher = (function() {
if (Conf['Current Board'] && (siteID !== Site.hostname || boardID !== g.BOARD.ID)) {
continue;
}
+ if (groupByBoard) {
+ all.push((cont = []));
+ }
for (threadID in threads) {
data = threads[threadID];
if (data && typeof data === 'object') {
- all.push({
+ (groupByBoard ? cont : all).push({
siteID: siteID,
boardID: boardID,
threadID: threadID,
@@ -20407,7 +20746,7 @@ ThreadWatcher = (function() {
return all;
},
makeLine: function(siteID, boardID, threadID, data) {
- var count, div, excerpt, fullID, link, ref, ref1, software, title, x;
+ var count, div, excerpt, fullID, link, page, ref, ref1, software, title, x;
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
x = $.el('a', {
className: 'fa fa-times',
@@ -20428,6 +20767,13 @@ ThreadWatcher = (function() {
title: excerpt,
className: 'watcher-link'
});
+ if (Conf['Show Page'] && (data.page != null)) {
+ page = $.el('span', {
+ textContent: "[" + data.page + "]",
+ className: 'watcher-page'
+ });
+ $.add(link, page);
+ }
if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] && (data.unread != null)) {
count = $.el('span', {
textContent: "(" + data.unread + ")",
@@ -20450,6 +20796,14 @@ ThreadWatcher = (function() {
if (data.isDead) {
$.addClass(div, 'dead-thread');
}
+ if (Conf['Show Page']) {
+ if (data.lastPage) {
+ $.addClass(div, 'last-page');
+ }
+ if (data.page != null) {
+ div.dataset.page = data.page;
+ }
+ }
if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
if (data.unread === 0) {
$.addClass(div, 'replies-read');
@@ -20465,10 +20819,10 @@ ThreadWatcher = (function() {
return div;
},
setPrefixes: function(threads) {
- var conflicts, conflicts2, i, j, len, len1, len2, prefix, prefixes, siteID, siteID2;
+ var conflicts, conflicts2, j, k, len, len1, len2, prefix, prefixes, siteID, siteID2;
prefixes = {};
- for (i = 0, len1 = threads.length; i < len1; i++) {
- siteID = threads[i].siteID;
+ for (j = 0, len1 = threads.length; j < len1; j++) {
+ siteID = threads[j].siteID;
if (siteID in prefixes) {
continue;
}
@@ -20479,8 +20833,8 @@ ThreadWatcher = (function() {
len++;
prefix = siteID.slice(0, len);
conflicts2 = [];
- for (j = 0, len2 = conflicts.length; j < len2; j++) {
- siteID2 = conflicts[j];
+ for (k = 0, len2 = conflicts.length; k < len2; k++) {
+ siteID2 = conflicts[k];
if (siteID2.slice(0, len) === prefix) {
conflicts2.push(siteID2);
} else if (prefixes[siteID2].length < len) {
@@ -20494,12 +20848,12 @@ ThreadWatcher = (function() {
return ThreadWatcher.prefixes = prefixes;
},
build: function() {
- var boardID, data, i, j, len1, len2, list, nodes, ref, ref1, refresher, siteID, thread, threadID, threads;
+ var boardID, data, j, len1, list, nodes, ref, siteID, thread, threadID, threads;
nodes = [];
threads = ThreadWatcher.getAll();
ThreadWatcher.setPrefixes(threads);
- for (i = 0, len1 = threads.length; i < len1; i++) {
- ref = threads[i], siteID = ref.siteID, boardID = ref.boardID, threadID = ref.threadID, data = ref.data;
+ for (j = 0, len1 = threads.length; j < len1; j++) {
+ ref = threads[j], siteID = ref.siteID, boardID = ref.boardID, threadID = ref.threadID, data = ref.data;
if ((data.excerpt == null) && siteID === Site.hostname && (thread = g.threads[boardID + "." + threadID]) && thread.OP) {
ThreadWatcher.db.extend({
boardID: boardID,
@@ -20514,22 +20868,17 @@ ThreadWatcher = (function() {
list = ThreadWatcher.list;
$.rmAll(list);
$.add(list, nodes);
- ThreadWatcher.refreshIcon();
- ref1 = ThreadWatcher.menu.refreshers;
- for (j = 0, len2 = ref1.length; j < len2; j++) {
- refresher = ref1[j];
- refresher();
- }
+ return ThreadWatcher.refreshIcon();
},
refresh: function() {
ThreadWatcher.build();
g.threads.forEach(function(thread) {
- var i, isWatched, len1, post, ref, toggler;
+ var isWatched, j, len1, post, ref, toggler;
isWatched = ThreadWatcher.isWatched(thread);
if (thread.OP) {
ref = [thread.OP].concat(slice.call(thread.OP.clones));
- for (i = 0, len1 = ref.length; i < len1; i++) {
- post = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ post = ref[j];
if ((toggler = $('.watch-thread-link', post.nodes.info))) {
ThreadWatcher.setToggler(toggler, isWatched);
}
@@ -20546,30 +20895,35 @@ ThreadWatcher = (function() {
}
},
refreshIcon: function() {
- var className, i, len1, ref;
+ var className, j, len1, ref;
ref = ['replies-unread', 'replies-quoting-you'];
- for (i = 0, len1 = ref.length; i < len1; i++) {
- className = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ className = ref[j];
ThreadWatcher.shortcut.classList.toggle(className, !!$("." + className, ThreadWatcher.dialog));
}
},
- update: function(boardID, threadID, newData) {
- var data, key, line, n, newLine, ref, siteID, val;
- siteID = Site.hostname;
+ update: function(siteID, boardID, threadID, newData) {
+ var data, j, key, len1, line, n, newLine, ref, ref1, val;
if (!(data = (ref = ThreadWatcher.db) != null ? ref.get({
+ siteID: siteID,
boardID: boardID,
threadID: threadID
}) : void 0)) {
return;
}
if (newData.isDead && Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- boardID: boardID,
- threadID: threadID
- });
- ThreadWatcher.refresh();
+ ThreadWatcher.rm(siteID, boardID, threadID);
return;
}
+ if (newData.isDead || newData.last === -1) {
+ ref1 = ['page', 'lastPage', 'unread', 'quotingyou'];
+ for (j = 0, len1 = ref1.length; j < len1; j++) {
+ key = ref1[j];
+ if (!(key in newData)) {
+ newData[key] = void 0;
+ }
+ }
+ }
n = 0;
for (key in newData) {
val = newData[key];
@@ -20580,18 +20934,13 @@ ThreadWatcher = (function() {
if (!n) {
return;
}
- if (!(data = ThreadWatcher.db.get({
- boardID: boardID,
- threadID: threadID
- }))) {
- return;
- }
ThreadWatcher.db.extend({
+ siteID: siteID,
boardID: boardID,
threadID: threadID,
val: newData
});
- if (line = $("#watched-threads > [data-site-i-d='" + siteID + "'][data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog)) {
+ if ((line = $("#watched-threads > [data-site-i-d='" + siteID + "'][data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog))) {
newLine = ThreadWatcher.makeLine(siteID, boardID, threadID, data);
$.replace(line, newLine);
return ThreadWatcher.refreshIcon();
@@ -20614,16 +20963,19 @@ ThreadWatcher = (function() {
});
return cb();
}
- if (data.isDead && !((data.unread != null) || (data.quotingYou != null))) {
+ if (data.isDead && !((data.page != null) || (data.lastPage != null) || (data.unread != null) || (data.quotingYou != null))) {
return cb();
}
return ThreadWatcher.db.extend({
boardID: boardID,
threadID: threadID,
val: {
- isDead: true
- },
- rm: ['unread', 'quotingYou']
+ isDead: true,
+ page: void 0,
+ lastPage: void 0,
+ unread: void 0,
+ quotingYou: void 0
+ }
}, cb);
},
toggle: function(thread) {
@@ -20662,19 +21014,24 @@ ThreadWatcher = (function() {
return ThreadWatcher.addRaw(boardID, threadID, data);
},
addRaw: function(boardID, threadID, data) {
+ var thread;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
ThreadWatcher.refresh();
- if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
- return ThreadWatcher.fetchStatus({
- siteID: Site.hostname,
- boardID: boardID,
- threadID: threadID,
- data: data
- }, true);
+ thread = {
+ siteID: Site.hostname,
+ boardID: boardID,
+ threadID: threadID,
+ data: data,
+ force: true
+ };
+ if (Conf['Show Page'] && !data.isDead) {
+ return ThreadWatcher.fetchBoard([thread]);
+ } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ return ThreadWatcher.fetchStatus(thread);
}
},
rm: function(siteID, boardID, threadID) {
@@ -20686,7 +21043,6 @@ ThreadWatcher = (function() {
return ThreadWatcher.refresh();
},
menu: {
- refreshers: [],
init: function() {
var menu;
if (!Conf['Thread Watcher']) {
@@ -20708,73 +21064,61 @@ ThreadWatcher = (function() {
});
Header.menu.addEntry({
el: entryEl,
- order: 60
+ order: 60,
+ open: function() {
+ var addClass, ref, rmClass, text;
+ ref = !!ThreadWatcher.db.get({
+ boardID: g.BOARD.ID,
+ threadID: g.THREADID
+ }) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = ref[0], rmClass = ref[1], text = ref[2];
+ $.addClass(entryEl, addClass);
+ $.rmClass(entryEl, rmClass);
+ entryEl.textContent = text;
+ return true;
+ }
});
- $.on(entryEl, 'click', function() {
+ return $.on(entryEl, 'click', function() {
return ThreadWatcher.toggle(g.threads[g.BOARD + "." + g.THREADID]);
});
- return this.refreshers.push(function() {
- var addClass, ref, rmClass, text;
- ref = $('.current', ThreadWatcher.list) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = ref[0], rmClass = ref[1], text = ref[2];
- $.addClass(entryEl, addClass);
- $.rmClass(entryEl, rmClass);
- return entryEl.textContent = text;
- });
},
addMenuEntries: function() {
- var cb, conf, entries, entry, i, len1, name, ref, ref1, refresh, subEntries;
+ var cb, conf, entries, entry, j, len1, name, open, ref, ref1, text;
entries = [];
entries.push({
+ text: 'Open all threads',
cb: ThreadWatcher.cb.openAll,
- entry: {
- el: $.el('a', {
- textContent: 'Open all threads'
- })
- },
- refresh: function() {
- return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled');
+ open: function() {
+ this.el.classList.toggle('disabled', !ThreadWatcher.list.firstElementChild);
+ return true;
}
});
entries.push({
+ text: 'Prune dead threads',
cb: ThreadWatcher.cb.pruneDeads,
- entry: {
+ open: function() {
+ this.el.classList.toggle('disabled', !$('.dead-thread', ThreadWatcher.list));
+ return true;
+ }
+ });
+ for (j = 0, len1 = entries.length; j < len1; j++) {
+ ref = entries[j], text = ref.text, cb = ref.cb, open = ref.open;
+ entry = {
el: $.el('a', {
- textContent: 'Prune dead threads'
+ textContent: text,
+ href: 'javascript:;'
})
- },
- refresh: function() {
- return ($('.dead-thread', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled');
- }
- });
- subEntries = [];
- ref = Config.threadWatcher;
- for (name in ref) {
- conf = ref[name];
- subEntries.push(this.createSubEntry(name, conf[1]));
- }
- entries.push({
- entry: {
- el: $.el('span', {
- textContent: 'Settings'
- }),
- subEntries: subEntries
- }
- });
- for (i = 0, len1 = entries.length; i < len1; i++) {
- ref1 = entries[i], entry = ref1.entry, cb = ref1.cb, refresh = ref1.refresh;
- if (entry.el.nodeName === 'A') {
- entry.el.href = 'javascript:;';
- }
- if (cb) {
- $.on(entry.el, 'click', cb);
- }
- if (refresh) {
- this.refreshers.push(refresh.bind(entry));
- }
+ };
+ $.on(entry.el, 'click', cb);
+ entry.open = open.bind(entry);
this.menu.addEntry(entry);
}
+ ref1 = Config.threadWatcher;
+ for (name in ref1) {
+ conf = ref1[name];
+ this.addCheckbox(name, conf[1]);
+ }
},
- createSubEntry: function(name, desc) {
+ addCheckbox: function(name, desc) {
var entry, input;
entry = {
type: 'thread watcher',
@@ -20788,13 +21132,13 @@ ThreadWatcher = (function() {
entry.el.title += '\n[Remember Last Read Post is disabled.]';
}
$.on(input, 'change', $.cb.checked);
- if (name === 'Current Board' || name === 'Show Unread Count' || name === 'Show Site Prefix') {
+ if (name === 'Current Board' || name === 'Show Page' || name === 'Show Unread Count' || name === 'Show Site Prefix') {
$.on(input, 'change', ThreadWatcher.refresh);
}
- if (name === 'Show Unread Count' || name === 'Auto Update Thread Watcher') {
+ if (name === 'Show Page' || name === 'Show Unread Count' || name === 'Auto Update Thread Watcher') {
$.on(input, 'change', ThreadWatcher.fetchAuto);
}
- return entry;
+ return this.menu.addEntry(entry);
}
}
};
@@ -20948,12 +21292,15 @@ Unread = (function() {
return;
}
},
- openNotification: function(post) {
+ openNotification: function(post, predicate) {
var notif;
+ if (predicate == null) {
+ predicate = ' replied to you';
+ }
if (!Header.areNotificationsEnabled) {
return;
}
- notif = new Notification(post.info.nameBlock + " replied to you", {
+ notif = new Notification("" + post.info.nameBlock + predicate, {
body: post.commentDisplay(),
icon: Favicon.logo
});
@@ -21083,7 +21430,7 @@ Unread = (function() {
saveThreadWatcherCount: $.debounce(2 * $.SECOND, function() {
$.forceSync('Remember Last Read Post');
if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) {
- return ThreadWatcher.update(Unread.thread.board.ID, Unread.thread.ID, {
+ return ThreadWatcher.update(Site.hostname, Unread.thread.board.ID, Unread.thread.ID, {
isDead: Unread.thread.isDead,
unread: Unread.posts.size,
quotingYou: !!(!Conf['Require OP Quote Link'] && QuoteYou.isYou(Unread.thread.OP) ? Unread.posts.size : Unread.postsQuotingYou.size)
@@ -21216,16 +21563,12 @@ UnreadIndex = (function() {
markRead: function() {
var lastPost, thread;
thread = Get.threadFromNode(this);
- if (Index.enabled) {
- lastPost = Index.lastPost(thread.ID);
- } else {
- lastPost = 0;
- thread.posts.forEach(function(post) {
- if (post.ID > lastPost && !post.isFetchedQuote) {
- return lastPost = post.ID;
- }
- });
- }
+ lastPost = Index.enabled ? Index.lastPost(thread.ID) : 0;
+ thread.posts.forEach(function(post) {
+ if (post.ID > lastPost && !post.isFetchedQuote) {
+ return lastPost = post.ID;
+ }
+ });
UnreadIndex.lastReadPost[thread.fullID] = lastPost;
UnreadIndex.db.set({
boardID: thread.board.ID,
@@ -21234,7 +21577,7 @@ UnreadIndex = (function() {
});
$.rm(UnreadIndex.hr[thread.fullID]);
thread.nodes.root.classList.remove('unread-thread');
- return ThreadWatcher.update(thread.board.ID, thread.ID, {
+ return ThreadWatcher.update(Site.hostname, thread.board.ID, thread.ID, {
unread: 0,
quotingYou: false
});
@@ -22729,7 +23072,7 @@ QR = (function() {
}
},
submit: function(e) {
- var captcha, cb, err, extra, filetag, formData, options, post, ref, thread, threadID;
+ var captcha, cb, err, filetag, formData, options, post, ref, thread, threadID;
if (e != null) {
e.preventDefault();
}
@@ -22799,47 +23142,35 @@ QR = (function() {
options = {
responseType: 'document',
withCredentials: true,
- onload: QR.response,
- onerror: function() {
- delete QR.req;
- if (QR.currentCaptcha) {
- Captcha.cache.save(QR.currentCaptcha);
- }
- delete QR.currentCaptcha;
- post.unlock();
- QR.cooldown.auto = true;
- QR.cooldown.addDelay(post, 2);
- QR.status();
- return QR.error(QR.connectionError());
- }
- };
- extra = {
+ onloadend: QR.response,
form: $.formData(formData)
};
if (Conf['Show Upload Progress']) {
- extra.upCallbacks = {
- onload: function() {
+ options.onprogress = function(e) {
+ var ref1;
+ if (this !== ((ref1 = QR.req) != null ? ref1.upload : void 0)) {
+ return;
+ }
+ if (e.loaded < e.total) {
+ QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%";
+ } else {
QR.req.isUploadFinished = true;
QR.req.progress = '...';
- return QR.status();
- },
- onprogress: function(e) {
- QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%";
- return QR.status();
}
+ return QR.status();
};
}
cb = function(response) {
if (response != null) {
QR.currentCaptcha = response;
if (response.challenge != null) {
- extra.form.append('recaptcha_challenge_field', response.challenge);
- extra.form.append('recaptcha_response_field', response.response);
+ options.form.append('recaptcha_challenge_field', response.challenge);
+ options.form.append('recaptcha_response_field', response.response);
} else {
- extra.form.append('g-recaptcha-response', response.response);
+ options.form.append('g-recaptcha-response', response.response);
}
}
- QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options, extra);
+ QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options);
return QR.req.progress = '...';
};
if (typeof captcha === 'function') {
@@ -22865,23 +23196,24 @@ QR = (function() {
return QR.status();
},
response: function() {
- var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, req, resDoc, seconds, threadID;
- req = QR.req;
+ var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, ref3, seconds, threadID;
+ if (this !== QR.req) {
+ return;
+ }
delete QR.req;
post = QR.posts[0];
post.unlock();
- resDoc = req.response;
- if ((err = resDoc.getElementById('errmsg'))) {
- if ((ref = $('a', err)) != null) {
- ref.target = '_blank';
+ if ((err = (ref = this.response) != null ? ref.getElementById('errmsg') : void 0)) {
+ if ((ref1 = $('a', err)) != null) {
+ ref1.target = '_blank';
}
- } else if ((connErr = resDoc.title !== 'Post successful!')) {
+ } else if ((connErr = !this.response || this.response.title !== 'Post successful!')) {
err = QR.connectionError();
if (QR.currentCaptcha) {
Captcha.cache.save(QR.currentCaptcha);
}
- } else if (req.status !== 200) {
- err = "Error " + req.statusText + " (" + req.status + ")";
+ } else if (this.status !== 200) {
+ err = "Error " + this.statusText + " (" + this.status + ")";
}
delete QR.currentCaptcha;
if (err) {
@@ -22904,13 +23236,13 @@ QR = (function() {
} else {
QR.cooldown.auto = false;
}
- QR.captcha.setup(QR.cooldown.auto && ((ref1 = d.activeElement) === QR.nodes.status || ref1 === d.body));
+ QR.captcha.setup(QR.cooldown.auto && ((ref2 = d.activeElement) === QR.nodes.status || ref2 === d.body));
QR.status();
QR.error(err);
return;
}
- h1 = $('h1', resDoc);
- ref2 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref2[0], threadID = ref2[1], postID = ref2[2];
+ h1 = $('h1', this.response);
+ ref3 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref3[0], threadID = ref3[1], postID = ref3[2];
postID = +postID;
threadID = +threadID || postID;
isReply = threadID !== postID;
@@ -22927,10 +23259,10 @@ QR = (function() {
postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount && isReply;
lastPostToThread = !((function() {
- var j, len, p, ref3;
- ref3 = QR.posts.slice(1);
- for (j = 0, len = ref3.length; j < len; j++) {
- p = ref3[j];
+ var j, len, p, ref4;
+ ref4 = QR.posts.slice(1);
+ for (j = 0, len = ref4.length; j < len; j++) {
+ p = ref4[j];
if (p.thread === post.thread) {
return true;
}
@@ -22981,17 +23313,18 @@ QR = (function() {
} else {
return setTimeout(check, attempts * $.SECOND);
}
- }
- }, {
+ },
+ responseType: 'text',
type: 'HEAD'
});
};
return check();
},
abort: function() {
- if (QR.req && !QR.req.isUploadFinished) {
- QR.req.abort();
+ var oldReq;
+ if ((oldReq = QR.req) && !QR.req.isUploadFinished) {
delete QR.req;
+ oldReq.abort();
if (QR.currentCaptcha) {
Captcha.cache.save(QR.currentCaptcha);
}
@@ -24669,6 +25002,14 @@ QuoteThreading =
parent: {},
children: {},
inserted: {},
+ toggleThreading: function() {
+ return this.setThreadingState(!Conf['Thread Quotes']);
+ },
+ setThreadingState: function(enabled) {
+ this.input.checked = enabled;
+ this.setEnabled.call(this.input);
+ return this.rethread.call(this.input);
+ },
setEnabled: function() {
var other, ref;
if (this.checked) {
@@ -25184,9 +25525,6 @@ Main = (function() {
Main = {
init: function() {
var db, flatten, i, items, j, k, key, len, ref, ref1, ref2, w;
- if (d.body && !$('title', d.head)) {
- return;
- }
try {
w = window;
if ($.platform === 'crx') {
diff --git a/builds/4chan-X.crx b/builds/4chan-X.crx
index 50f05c72c..7e79313c4 100644
Binary files a/builds/4chan-X.crx and b/builds/4chan-X.crx differ
diff --git a/builds/4chan-X.meta.js b/builds/4chan-X.meta.js
index 26daf5353..f931ee9fe 100644
--- a/builds/4chan-X.meta.js
+++ b/builds/4chan-X.meta.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
-// @version 1.14.5.13
+// @version 1.14.7.2
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js
index 791bd293b..3a268c150 100644
--- a/builds/4chan-X.user.js
+++ b/builds/4chan-X.user.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
-// @version 1.14.5.13
+// @version 1.14.7.2
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@@ -198,7 +198,7 @@ docSet = function() {
};
g = {
- VERSION: '1.14.5.13',
+ VERSION: '1.14.7.2',
NAMESPACE: '4chan X.',
boards: {}
};
@@ -241,6 +241,7 @@ Config = (function() {
'JSON Index': [true, 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'],
'Use 4chan X Catalog': [true, 'Link to 4chan X\'s catalog instead of the native 4chan one.', 1],
'Index Refresh Notifications': [false, 'Show a notice at the top of the page when the index is refreshed.', 1],
+ 'Follow Cursor': [true, 'Image Hover and Quote Preview move with the mouse cursor.'],
'Open Threads in New Tab': [false, 'Make links to threads in the index / 4chan X catalog open in a new tab.'],
'External Catalog': [false, 'Link to external catalog instead of the internal one.'],
'Catalog Links': [false, 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'],
@@ -410,6 +411,7 @@ Config = (function() {
'Auto Watch': [true, 'Automatically watch threads you start.'],
'Auto Watch Reply': [true, 'Automatically watch threads you reply to.'],
'Auto Prune': [false, 'Automatically remove dead threads.'],
+ 'Show Page': [true, 'Show what page watched threads are on.'],
'Show Unread Count': [true, 'Show number of unread posts in watched threads.'],
'Show Site Prefix': [true, 'When multiple sites are shown in the thread watcher, add a prefix to board names to distinguish them.'],
'Require OP Quote Link': [false, 'For purposes of thread watcher highlighting, only consider posts with a quote link to the OP as replies to the OP.']
@@ -422,6 +424,7 @@ Config = (function() {
tripcode: "# Filter any tripfag\n#/^!/",
capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for admins:\n#/Admin$/;highlight:admin;op:yes",
pass: "# Filter anyone using since4pass:\n#/./",
+ email: '',
subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only",
comment: "# Filter Stallman copypasta on /g/:\n#/what you\'re refer+ing to as linux/i;boards:g\n# Filter posts with 20 or more quote links:\n#/(?:>>\\d(?:(?!>>\\d)[^])*){20}/\n# Filter posts like T H I S / H / I / S:\n#/^>?\\s?\\w\\s?(\\w)\\s?(\\w)\\s?(\\w).*$[\\s>]+\\1[\\s>]+\\2[\\s>]+\\3/im",
flag: '',
@@ -430,7 +433,7 @@ Config = (function() {
filesize: '',
MD5: ''
},
- sauces: "# Known filename formats:\nhttp://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/\n//%$1.deviantart.com/gallery/#/d%$2;regexp:/^\\w+_by_(\\w+)-d([\\da-z]+)/\n//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/\nhttp://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/\nhttps://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/\n\n# Reverse image search:\nhttps://www.google.com/searchbyimage?image_url=%IMG&safe=off\nhttps://www.yandex.com/images/search?rpt=imageview&img_url=%IMG\n#//tineye.com/search?url=%IMG\n#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights\n\n# Specialized reverse image search:\n//iqdb.org/?url=%IMG\nhttps://trace.moe/?auto&url=%IMG;text:wait\n#//3d.iqdb.org/?url=%IMG\n#//saucenao.com/search.php?url=%IMG\n\n# \"View Same\" in archives:\nhttp://eye.swfchan.com/search/?q=%name;types:swf\n#https://desuarchive.org/_/search/image/%sMD5/\n#https://archive.4plebs.org/_/search/image/%sMD5/\n#https://boards.fireden.net/_/search/image/%sMD5/\n#https://foolz.fireden.net/_/search/image/%sMD5/\n\n# Other tools:\n#http://exif.regex.info/exif.cgi?imgurl=%URL\n#//imgops.com/%URL;types:gif,jpg,png\n#//www.gif-explode.com/%URL;types:gif",
+ sauces: "# Known filename formats:\nhttp://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/\n//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/\n//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/\nhttp://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/\nhttps://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/\n\n# Reverse image search:\nhttps://www.google.com/searchbyimage?image_url=%IMG&safe=off\nhttps://www.yandex.com/images/search?rpt=imageview&img_url=%IMG\n#//tineye.com/search?url=%IMG\n#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights\n\n# Specialized reverse image search:\n//iqdb.org/?url=%IMG\nhttps://trace.moe/?auto&url=%IMG;text:wait\n#//3d.iqdb.org/?url=%IMG\n#//saucenao.com/search.php?url=%IMG\n\n# \"View Same\" in archives:\nhttp://eye.swfchan.com/search/?q=%name;types:swf\n#https://desuarchive.org/_/search/image/%sMD5/\n#https://archive.4plebs.org/_/search/image/%sMD5/\n#https://boards.fireden.net/_/search/image/%sMD5/\n#https://foolz.fireden.net/_/search/image/%sMD5/\n\n# Other tools:\n#http://exif.regex.info/exif.cgi?imgurl=%URL\n#//imgops.com/%URL;types:gif,jpg,png\n#//www.gif-explode.com/%URL;types:gif",
FappeT: {
werk: false
},
@@ -497,6 +500,7 @@ Config = (function() {
'Update': ['r', 'Update the thread / refresh the index.'],
'Update thread watcher': ['Shift+r', 'Manually refresh thread watcher.'],
'Toggle thread watcher': ['t', 'Toggle visibility of thread watcher.'],
+ 'Toggle threading': ['Shift+t', 'Toggle threading.'],
'Mark thread read': ['Ctrl+0', 'Mark thread read from index (requires "Unread Line in Index").'],
'Expand image': ['Shift+e', 'Expand selected image.'],
'Expand images': ['e', 'Expand all images.'],
@@ -1390,6 +1394,10 @@ body.is_catalog .thread > a > img {\n\
.nwsb {\n\
display: inline;\n\
}\n\
+.fileText {\n\
+ max-width: auto;\n\
+ white-space: normal;\n\
+}\n\
/* Ads */\n\
.ad-cnt > *, .adg-rects > *, .bsa-cnt {\n\
height: auto !important;\n\
@@ -2412,12 +2420,11 @@ span.hide-announcement {\n\
-webkit-flex-direction: row;\n\
flex-direction: row;\n\
}\n\
+#watched-threads .watcher-page,\n\
#watched-threads .watcher-unread {\n\
-webkit-flex: 0 0 auto;\n\
flex: 0 0 auto;\n\
-}\n\
-#watched-threads .watcher-unread::after {\n\
- content: \"\\00a0\";\n\
+ margin-right: 2px;\n\
}\n\
#watched-threads .watcher-title {\n\
overflow: hidden;\n\
@@ -2425,7 +2432,10 @@ span.hide-announcement {\n\
-webkit-flex: 0 1 auto;\n\
flex: 0 1 auto;\n\
}\n\
-.replies-quoting-you > a, #watcher-link.replies-quoting-you {\n\
+#watched-threads .watcher-title:not(:first-child) {\n\
+ margin-left: 2px;\n\
+}\n\
+.replies-quoting-you > a, #watcher-link.replies-quoting-you, .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
#thread-watcher a {\n\
@@ -2587,6 +2597,13 @@ span.hide-announcement {\n\
.fileThumb > .warning {\n\
clear: both;\n\
}\n\
+#ihover {\n\
+ pointer-events: none;\n\
+ /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */\n\
+ max-height: 95vh;\n\
+ max-height: calc(100vh - 25px);\n\
+ max-width: 100vw;\n\
+}\n\
/* WEBM Metadata */\n\
.webm-title > a::before {\n\
content: \"title\";\n\
@@ -3699,7 +3716,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(240,224,214,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you {\n\
+:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you, :root.yotsuba .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -3881,7 +3898,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(240,224,214,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you {\n\
+:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you, :root.futaba .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -3974,7 +3991,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(214,218,240,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you {\n\
+:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you, :root.burichan .last-page > a > .watcher-page {\n\
color: #F00;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4135,7 +4152,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(40,42,46,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you {\n\
+:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you, :root.tomorrow .last-page > a > .watcher-page {\n\
color: #F00 !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4227,7 +4244,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(221,221,221,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you {\n\
+:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you, :root.photon .last-page > a > .watcher-page {\n\
color: #00F !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4382,7 +4399,7 @@ a:only-of-type > .remove {\n\
background-color: rgba(23,21,38,0.5);\n\
}\n\
/* Thread Watcher */\n\
-:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you {\n\
+:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you, :root.spooky .last-page > a > .watcher-page {\n\
color: #F00 !important;\n\
}\n\
/* Watcher Favicon */\n\
@@ -4595,58 +4612,43 @@ $ = (function() {
};
$.ajax = (function() {
- var lastModified, pageXHR;
- lastModified = {};
+ var pageXHR;
if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) {
pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest);
} else {
pageXHR = XMLHttpRequest;
}
- return function(url, options, extra) {
- var bypassCache, err, event, form, j, len, params, r, ref, ref1, type, upCallbacks, url0, whenModified;
+ return function(url, options) {
+ var err, form, headers, key, onloadend, onprogress, r, ref, responseType, timeout, type, value, withCredentials;
if (options == null) {
options = {};
}
- if (extra == null) {
- extra = {};
- }
- type = extra.type, whenModified = extra.whenModified, bypassCache = extra.bypassCache, upCallbacks = extra.upCallbacks, form = extra.form;
- if (/\.json$/.test(url)) {
- if (options.responseType == null) {
- options.responseType = 'json';
- }
+ onloadend = options.onloadend, timeout = options.timeout, responseType = options.responseType, withCredentials = options.withCredentials, type = options.type, onprogress = options.onprogress, form = options.form, headers = options.headers;
+ if (responseType == null) {
+ responseType = 'json';
}
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/');
- if (whenModified) {
- params = [];
- if ($.engine === 'blink') {
- params.push("s=" + whenModified);
- }
- if (Site.software === 'yotsuba' && bypassCache) {
- params.push("t=" + (Date.now()));
- }
- url0 = url;
- if (params.length) {
- url += '?' + params.join('&');
- }
- }
r = new pageXHR();
type || (type = form && 'post' || 'get');
try {
r.open(type, url, true);
- if (whenModified) {
- if (((ref = lastModified[whenModified]) != null ? ref[url0] : void 0) != null) {
- r.setRequestHeader('If-Modified-Since', lastModified[whenModified][url0]);
- }
- $.on(r, 'load', function() {
- return (lastModified[whenModified] || (lastModified[whenModified] = {}))[url0] = r.getResponseHeader('Last-Modified');
- });
+ ref = headers || {};
+ for (key in ref) {
+ value = ref[key];
+ r.setRequestHeader(key, value);
}
- $.extend(r, options);
- $.extend(r.upload, upCallbacks);
+ $.extend(r, {
+ onloadend: onloadend,
+ timeout: timeout,
+ responseType: responseType,
+ withCredentials: withCredentials
+ });
+ $.extend(r.upload, {
+ onprogress: onprogress
+ });
$.on(r, 'error', function() {
if (!r.status) {
- return c.error("4chan X failed to load: " + url);
+ return c.warn("4chan X failed to load: " + url);
}
});
r.send(form);
@@ -4655,51 +4657,82 @@ $ = (function() {
if (err.result !== 0x805e0006) {
throw err;
}
- ref1 = ['error', 'loadend'];
- for (j = 0, len = ref1.length; j < len; j++) {
- event = ref1[j];
- r["on" + event] = options["on" + event];
- $.queueTask($.event, event, null, r);
- }
+ r.onloadend = onloadend;
+ $.queueTask($.event, 'error', null, r);
+ $.queueTask($.event, 'loadend', null, r);
}
return r;
};
})();
+ $.lastModified = {};
+
+ $.whenModified = function(url, bucket, cb, options) {
+ var ajax, headers, params, r, ref, t, timeout, url0;
+ if (options == null) {
+ options = {};
+ }
+ timeout = options.timeout, ajax = options.ajax;
+ params = [];
+ if ($.engine === 'blink') {
+ params.push("s=" + bucket);
+ }
+ if (url.split('/')[2] === 'a.4cdn.org') {
+ params.push("t=" + (Date.now()));
+ }
+ url0 = url;
+ if (params.length) {
+ url += '?' + params.join('&');
+ }
+ headers = {};
+ if ((t = (ref = $.lastModified[bucket]) != null ? ref[url0] : void 0) != null) {
+ headers['If-Modified-Since'] = t;
+ }
+ r = (ajax || $.ajax)(url, {
+ onloadend: function() {
+ var base;
+ ((base = $.lastModified)[bucket] || (base[bucket] = {}))[url0] = this.getResponseHeader('Last-Modified');
+ return cb.call(this);
+ },
+ timeout: timeout,
+ headers: headers
+ });
+ return r;
+ };
+
(function() {
var reqs;
reqs = {};
$.cache = function(url, cb, options) {
- var err, req, rm;
- if (req = reqs[url]) {
- if (req.readyState === 4) {
- $.queueTask(function() {
- return cb.call(req, req.evt, true);
- });
- } else {
+ var ajax, onloadend, req;
+ if (options == null) {
+ options = {};
+ }
+ ajax = options.ajax;
+ if ((req = reqs[url])) {
+ if (req.callbacks) {
req.callbacks.push(cb);
+ } else {
+ $.queueTask(function() {
+ return cb.call(req, {
+ isCached: true
+ });
+ });
}
return req;
}
- rm = function() {
- return delete reqs[url];
- };
- try {
- if (!(req = $.ajax(url, options))) {
- return;
- }
- } catch (_error) {
- err = _error;
- return;
- }
- $.on(req, 'load', function(e) {
+ onloadend = function() {
var fn1, j, len, ref;
- this.evt = e;
+ if (!this.status) {
+ delete reqs[url];
+ }
ref = this.callbacks;
fn1 = (function(_this) {
return function(cb) {
return $.queueTask(function() {
- return cb.call(_this, e, false);
+ return cb.call(_this, {
+ isCached: false
+ });
});
};
})(this);
@@ -4708,8 +4741,10 @@ $ = (function() {
fn1(cb);
}
return delete this.callbacks;
+ };
+ req = (ajax || $.ajax)(url, {
+ onloadend: onloadend
});
- $.on(req, 'abort error', rm);
req.callbacks = [cb];
return reqs[url] = req;
};
@@ -5402,25 +5437,25 @@ $$ = (function() {
}).call(this);
CrossOrigin = (function() {
- var CrossOrigin;
+ var CrossOrigin, Request;
CrossOrigin = {
binary: function(url, cb, headers) {
- var options, ref, workaround;
if (headers == null) {
headers = {};
}
url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/');
- workaround = $.engine === 'gecko' && (typeof GM_info !== "undefined" && GM_info !== null) && /^[0-2]\.|^3\.[01](?!\d)/.test(GM_info.version);
- workaround || (workaround = /PaleMoon\//.test(navigator.userAgent));
- workaround || (workaround = (typeof GM_info !== "undefined" && GM_info !== null ? (ref = GM_info.script) != null ? ref.includeJSB : void 0 : void 0) != null);
- options = {
+ return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
method: "GET",
url: url,
headers: headers,
+ responseType: 'arraybuffer',
+ overrideMimeType: 'text/plain; charset=x-user-defined',
onload: function(xhr) {
- var contentDisposition, contentType, data, i, r, ref1, ref2;
- if (workaround) {
+ var data, i, r;
+ if (xhr.response instanceof ArrayBuffer) {
+ data = new Uint8Array(xhr.response);
+ } else {
r = xhr.responseText;
data = new Uint8Array(r.length);
i = 0;
@@ -5428,12 +5463,8 @@ CrossOrigin = (function() {
data[i] = r.charCodeAt(i);
i++;
}
- } else {
- data = new Uint8Array(xhr.response);
}
- contentType = (ref1 = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)) != null ? ref1[1] : void 0;
- contentDisposition = (ref2 = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)) != null ? ref2[1] : void 0;
- return cb(data, contentType, contentDisposition);
+ return cb(data, xhr.responseHeaders);
},
onerror: function() {
return cb(null);
@@ -5441,27 +5472,23 @@ CrossOrigin = (function() {
onabort: function() {
return cb(null);
}
- };
- if (workaround) {
- options.overrideMimeType = 'text/plain; charset=x-user-defined';
- } else {
- options.responseType = 'arraybuffer';
- }
- return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)(options);
+ });
},
file: function(url, cb) {
- return CrossOrigin.binary(url, function(data, contentType, contentDisposition) {
- var blob, match, mime, name, ref, ref1, ref2, ref3;
+ return CrossOrigin.binary(url, function(data, headers) {
+ var blob, contentDisposition, contentType, match, mime, name, ref, ref1, ref2, ref3, ref4;
if (data == null) {
return cb(null);
}
- name = (ref = url.match(/([^\/]+)\/*$/)) != null ? ref[1] : void 0;
+ name = (ref = url.match(/([^\/?#]+)\/*(?:$|[?#])/)) != null ? ref[1] : void 0;
+ contentType = (ref1 = headers.match(/Content-Type:\s*(.*)/i)) != null ? ref1[1] : void 0;
+ contentDisposition = (ref2 = headers.match(/Content-Disposition:\s*(.*)/i)) != null ? ref2[1] : void 0;
mime = (contentType != null ? contentType.match(/[^;]*/)[0] : void 0) || 'application/octet-stream';
- match = (contentDisposition != null ? (ref1 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref1[1] : void 0 : void 0) || (contentType != null ? (ref2 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref2[1] : void 0 : void 0);
+ match = (contentDisposition != null ? (ref3 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref3[1] : void 0 : void 0) || (contentType != null ? (ref4 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? ref4[1] : void 0 : void 0);
if (match) {
name = match.replace(/\\"/g, '"');
}
- if ((typeof GM_info !== "undefined" && GM_info !== null ? (ref3 = GM_info.script) != null ? ref3.includeJSB : void 0 : void 0) != null) {
+ if (/^text\/plain;\s*charset=x-user-defined$/i.test(mime)) {
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] || 'application/octet-stream';
}
blob = new Blob([data], {
@@ -5471,95 +5498,94 @@ CrossOrigin = (function() {
return cb(blob);
});
},
- json: (function() {
- var callbacks, failure, results, success;
- callbacks = {};
- results = {};
- success = function(url, result) {
- var cb, j, len, ref;
- ref = callbacks[url];
- for (j = 0, len = ref.length; j < len; j++) {
- cb = ref[j];
- $.queueTask(function() {
- return cb.call(result);
- });
- }
- delete callbacks[url];
- return results[url] = result;
- };
- failure = function(url) {
- var cb, j, len, ref;
- ref = callbacks[url];
- for (j = 0, len = ref.length; j < len; j++) {
- cb = ref[j];
- $.queueTask(function() {
- return cb.call({});
- });
- }
- return delete callbacks[url];
- };
- return function(url, cb, bypassCache, timeout) {
- var req;
- if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) {
- if (bypassCache) {
- $.cleanCache(function(url2) {
- return url2 === url;
- });
- }
- if ((req = $.cache(url, cb, {
- responseType: 'json'
- }))) {
- $.on(req, 'abort error', function() {
- return cb.call({});
- });
- } else {
- cb.call({});
- }
- return;
- }
- if (bypassCache) {
- delete results[url];
- } else {
- if (results[url]) {
- cb.call(results[url]);
- return;
- }
- if (callbacks[url]) {
- callbacks[url].push(cb);
- return;
- }
- }
- callbacks[url] = [cb];
- return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
- method: "GET",
- url: url + '',
- timeout: timeout,
- onload: function(xhr) {
- var response, status, statusText;
- status = xhr.status, statusText = xhr.statusText;
- try {
- response = JSON.parse(xhr.responseText);
- return success(url, {
- status: status,
- statusText: statusText,
- response: response
- });
- } catch (_error) {
- return failure(url);
+ Request: Request = (function() {
+ function Request() {}
+
+ Request.prototype.status = 0;
+
+ Request.prototype.statusText = '';
+
+ Request.prototype.response = null;
+
+ Request.prototype.responseHeaderString = null;
+
+ Request.prototype.getResponseHeader = function(headerName) {
+ var header, i, j, key, len, ref, ref1, val;
+ if ((this.responseHeaders == null) && (this.responseHeaderString != null)) {
+ this.responseHeaders = {};
+ ref = this.responseHeaderString.split('\r\n');
+ for (j = 0, len = ref.length; j < len; j++) {
+ header = ref[j];
+ if ((i = header.indexOf(':')) >= 0) {
+ key = header.slice(0, i).trim().toLowerCase();
+ val = header.slice(i + 1).trim();
+ this.responseHeaders[key] = val;
}
- },
- onerror: function() {
- return failure(url);
- },
- onabort: function() {
- return failure(url);
- },
- ontimeout: function() {
- return failure(url);
}
- });
+ }
+ return (ref1 = (this.responseHeaders || {})[headerName.toLowerCase()]) != null ? ref1 : null;
};
+
+ Request.prototype.abort = function() {};
+
+ Request.prototype.onloadend = function() {};
+
+ return Request;
+
})(),
+ ajax: function(url, options) {
+ var gmReq, headers, onloadend, req, timeout;
+ if (options == null) {
+ options = {};
+ }
+ onloadend = options.onloadend, timeout = options.timeout, headers = options.headers;
+ if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) {
+ return $.ajax(url, options);
+ }
+ req = new CrossOrigin.Request();
+ req.onloadend = onloadend;
+ gmReq = ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({
+ method: 'GET',
+ url: url,
+ headers: headers,
+ timeout: timeout,
+ onload: function(xhr) {
+ var response;
+ try {
+ response = xhr.responseText ? JSON.parse(xhr.responseText) : null;
+ $.extend(req, {
+ response: response,
+ status: xhr.status,
+ statusText: xhr.statusText,
+ responseHeaderString: xhr.responseHeaders
+ });
+ } catch (_error) {}
+ return req.onloadend();
+ },
+ onerror: function() {
+ return req.onloadend();
+ },
+ onabort: function() {
+ return req.onloadend();
+ },
+ ontimeout: function() {
+ return req.onloadend();
+ }
+ });
+ if (gmReq && typeof gmReq.abort === 'function') {
+ req.abort = function() {
+ try {
+ return gmReq.abort();
+ } catch (_error) {}
+ };
+ }
+ return req;
+ },
+ cache: function(url, cb) {
+ return $.cache(url, cb, {
+ ajax: CrossOrigin.ajax
+ });
+ },
permission: function(cb) {
return cb();
}
@@ -5637,12 +5663,12 @@ Callbacks = (function() {
return this[name] = cb;
};
- Callbacks.prototype.execute = function(node, keys) {
+ Callbacks.prototype.execute = function(node, keys, force) {
var err, errors, i, len, name, ref, ref1, ref2;
if (keys == null) {
keys = this.keys;
}
- if (node.callbacksExecuted) {
+ if (node.callbacksExecuted && !force) {
return;
}
node.callbacksExecuted = true;
@@ -5764,7 +5790,7 @@ DataBoard = (function() {
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
DataBoard = (function() {
- DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles'];
+ DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles'];
function DataBoard(key1, sync, dontClean) {
var init;
@@ -5942,24 +5968,26 @@ DataBoard = (function() {
};
DataBoard.prototype.extend = function(arg, cb) {
- var boardID, postID, rm, siteID, threadID, val;
- siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, postID = arg.postID, val = arg.val, rm = arg.rm;
+ var boardID, postID, siteID, threadID, val;
+ siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, postID = arg.postID, val = arg.val;
return this.save((function(_this) {
return function() {
- var i, key, len, oldVal, ref;
+ var key, oldVal, subVal;
oldVal = _this.get({
siteID: siteID,
boardID: boardID,
threadID: threadID,
postID: postID,
- val: {}
+ defaultValue: {}
});
- ref = rm || [];
- for (i = 0, len = ref.length; i < len; i++) {
- key = ref[i];
- delete oldVal[key];
+ for (key in val) {
+ subVal = val[key];
+ if (typeof subVal === 'undefined') {
+ delete oldVal[key];
+ } else {
+ oldVal[key] = subVal;
+ }
}
- $.extend(oldVal, val);
return _this.setUnsafe({
siteID: siteID,
boardID: boardID,
@@ -5971,10 +5999,13 @@ DataBoard = (function() {
})(this), cb);
};
- DataBoard.prototype.setLastChecked = function() {
+ DataBoard.prototype.setLastChecked = function(key) {
+ if (key == null) {
+ key = 'lastChecked';
+ }
return this.save((function(_this) {
return function() {
- return _this.data.lastChecked = Date.now();
+ return _this.data[key] = Date.now();
};
})(this));
};
@@ -6005,9 +6036,6 @@ DataBoard = (function() {
DataBoard.prototype.clean = function() {
var boardID, now, ref, ref1, siteID, val;
- if (Site.software !== 'yotsuba') {
- return;
- }
siteID = Site.hostname;
ref = this.data[siteID].boards;
for (boardID in ref) {
@@ -6027,21 +6055,36 @@ DataBoard = (function() {
};
DataBoard.prototype.ajaxClean = function(boardID) {
- return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/threads.json", (function(_this) {
- return function(e1) {
- var response1;
- if (e1.target.status !== 200) {
+ var base, siteID, that, threadsList;
+ that = this;
+ siteID = Site.hostname;
+ threadsList = typeof (base = Site.urls).threadsListJSON === "function" ? base.threadsListJSON({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0;
+ if (!threadsList) {
+ return;
+ }
+ return $.cache(threadsList, function() {
+ var archiveList, base1, response1;
+ if (this.status !== 200) {
+ return;
+ }
+ archiveList = typeof (base1 = Site.urls).archiveListJSON === "function" ? base1.archiveListJSON({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0;
+ if (!archiveList) {
+ return that.ajaxCleanParse(boardID, this.response);
+ }
+ response1 = this.response;
+ return $.cache(archiveList, function() {
+ if (this.status !== 200) {
return;
}
- response1 = e1.target.response;
- return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/archive.json", function(e2) {
- if (!(e2.target.status === 200 || (boardID === 'b' || boardID === 'f' || boardID === 'trash' || boardID === 'bant'))) {
- return;
- }
- return _this.ajaxCleanParse(boardID, response1, e2.target.response);
- });
- };
- })(this));
+ return that.ajaxCleanParse(boardID, response1, this.response);
+ });
+ });
};
DataBoard.prototype.ajaxCleanParse = function(boardID, response1, response2) {
@@ -6102,7 +6145,7 @@ Fetcher = (function() {
Fetcher = (function() {
function Fetcher(boardID1, threadID, postID1, root, quoter) {
- var board, post, ref, thread;
+ var board, post, ref, that, thread;
this.boardID = boardID1;
this.threadID = threadID;
this.postID = postID1;
@@ -6122,11 +6165,15 @@ Fetcher = (function() {
}
this.root.textContent = "Loading post No." + this.postID + "...";
if (this.threadID) {
- $.cache(location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json", (function(_this) {
- return function(e, isCached) {
- return _this.fetchedPost(e.target, isCached);
- };
- })(this));
+ that = this;
+ $.cache(Site.urls.threadJSON({
+ boardID: this.boardID,
+ threadID: this.threadID
+ }), function(arg) {
+ var isCached;
+ isCached = arg.isCached;
+ return that.fetchedPost(this, isCached);
+ });
} else {
this.archivedPost();
}
@@ -6165,18 +6212,18 @@ Fetcher = (function() {
};
Fetcher.prototype.fetchedPost = function(req, isCached) {
- var api, board, k, len, post, posts, status, thread;
+ var api, board, k, len, post, posts, status, that, thread;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
}
status = req.status;
if (status !== 200) {
- if (this.archivedPost()) {
+ if (status && this.archivedPost()) {
return;
}
$.addClass(this.root, 'warning');
- this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : "Error " + req.statusText + " (" + req.status + ").";
+ this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : !status ? 'Connection Error' : "Error " + req.statusText + " (" + req.status + ").";
return;
}
posts = req.response.posts;
@@ -6189,15 +6236,17 @@ Fetcher = (function() {
}
if (post.no !== this.postID) {
if (isCached) {
- api = location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json";
+ api = Site.urls.threadJSON({
+ boardID: this.boardID,
+ threadID: this.threadID
+ });
$.cleanCache(function(url) {
return url === api;
});
- $.cache(api, (function(_this) {
- return function(e) {
- return _this.fetchedPost(e.target, false);
- };
- })(this));
+ that = this;
+ $.cache(api, function() {
+ return that.fetchedPost(this, false);
+ });
return;
}
if (this.archivedPost()) {
@@ -6230,7 +6279,7 @@ Fetcher = (function() {
encryptionOK = /^https:\/\//.test(url) || location.protocol === 'http:';
if (encryptionOK || Conf['Exempt Archives from Encryption']) {
that = this;
- CrossOrigin.json(url, function() {
+ CrossOrigin.cache(url, function() {
var key, media, ref, ref1;
if (!encryptionOK && ((ref = this.response) != null ? ref.media : void 0)) {
media = this.response.media;
@@ -6542,6 +6591,7 @@ Post = (function() {
this.ID = +root.id.match(/\d*$/)[0];
this.threadID = this.thread.ID;
this.boardID = this.board.ID;
+ this.siteID = Site.hostname;
this.fullID = this.board + "." + this.ID;
this.context = this;
this.isReply = this.ID !== this.threadID;
@@ -6562,6 +6612,7 @@ Post = (function() {
this.info = {
subject: ((ref1 = this.nodes.subject) != null ? ref1.textContent : void 0) || void 0,
name: (ref2 = this.nodes.name) != null ? ref2.textContent : void 0,
+ email: this.nodes.email ? decodeURIComponent(this.nodes.email.href.replace(/^mailto:/, '')) : void 0,
tripcode: (ref3 = this.nodes.tripcode) != null ? ref3.textContent : void 0,
uniqueID: (ref4 = this.nodes.uniqueID) != null ? ref4.textContent : void 0,
capcode: (ref5 = this.nodes.capcode) != null ? ref5.textContent.replace('## ', '') : void 0,
@@ -7323,7 +7374,7 @@ SW = {};
SW.tinyboard = {
isOPContainerThread: true,
mayLackJSON: true,
- disabledFeatures: ['Board Configuration', 'Normalize URL', 'Captcha Configuration', 'Image Host Rewriting', 'Index Generator', 'Announcement Hiding', 'Fourchan thingies', 'Resurrect Quotes', 'Quick Reply Personas', 'Quick Reply', 'Cooldown', 'Pass Link', 'Index Generator (Menu)', 'Report Link', 'Delete Link', 'Edit Link', 'Archive Link', 'Quote Inlining', 'Quote Previewing', 'Quote Backlinks', 'File Info Formatting', 'Fappe Tyme', 'Image Expansion', 'Image Expansion (Menu)', 'Comment Expansion', 'Thread Expansion', 'Favicon', 'Quote Threading', 'Thread Stats', 'Thread Updater', 'Mark New IPs', 'Banner', 'Flash Features', 'Reply Pruning'],
+ disabledFeatures: ['Board Configuration', 'Normalize URL', 'Captcha Configuration', 'Image Host Rewriting', 'Index Generator', 'Announcement Hiding', 'Resurrect Quotes', 'Quick Reply Personas', 'Quick Reply', 'Cooldown', 'Pass Link', 'Index Generator (Menu)', 'Report Link', 'Delete Link', 'Edit Link', 'Archive Link', 'Quote Inlining', 'Quote Previewing', 'Quote Backlinks', 'File Info Formatting', 'Fappe Tyme', 'Image Expansion', 'Image Expansion (Menu)', 'Comment Expansion', 'Thread Expansion', 'Favicon', 'Quote Threading', 'Thread Stats', 'Thread Updater', 'Mark New IPs', 'Banner', 'Flash Features', 'Reply Pruning'],
detect: function() {
var i, len, m, properties, ref, root, script;
ref = $$('script:not([src])', d.head);
@@ -7359,6 +7410,26 @@ SW = {};
} else {
return '';
}
+ },
+ threadsListJSON: function(arg) {
+ var boardID, ref, root, siteID;
+ siteID = arg.siteID, boardID = arg.boardID;
+ root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0;
+ if (root) {
+ return "" + root + boardID + "/threads.json";
+ } else {
+ return '';
+ }
+ },
+ catalogJSON: function(arg) {
+ var boardID, ref, root, siteID;
+ siteID = arg.siteID, boardID = arg.boardID;
+ root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0;
+ if (root) {
+ return "" + root + boardID + "/catalog.json";
+ } else {
+ return '';
+ }
}
},
selectors: {
@@ -7475,8 +7546,32 @@ SW = {};
var boardID, threadID;
boardID = arg.boardID, threadID = arg.threadID;
return location.protocol + "//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json";
+ },
+ threadsListJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return location.protocol + "//a.4cdn.org/" + boardID + "/threads.json";
+ },
+ archiveListJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ if (BoardConfig.isArchived(boardID)) {
+ return location.protocol + "//a.4cdn.org/" + boardID + "/archive.json";
+ } else {
+ return '';
+ }
+ },
+ catalogJSON: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return location.protocol + "//a.4cdn.org/" + boardID + "/catalog.json";
}
},
+ isPrunedByAge: function(arg) {
+ var boardID;
+ boardID = arg.boardID;
+ return boardID === 'f';
+ },
selectors: {
board: '.board',
thread: '.thread',
@@ -7565,7 +7660,10 @@ SW = {};
thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : void 0;
if (g.BOARD.ID === 'f' && thread.OP.file) {
file = thread.OP.file;
- return $.ajax(location.protocol + "//a.4cdn.org/f/thread/" + thread + ".json", {
+ return $.ajax(Site.urls.threadJSON({
+ boardID: 'f',
+ threadID: thread.ID
+ }), {
timeout: $.MINUTE,
onloadend: function() {
if (this.response) {
@@ -7652,6 +7750,9 @@ SW = {};
},
hasCORS: function(url) {
return url.split('/').slice(0, 3).join('/') === location.protocol + '//a.4cdn.org';
+ },
+ sfwBoards: function(sfw) {
+ return BoardConfig.sfwBoards(sfw);
}
};
@@ -7666,22 +7767,27 @@ Site = (function() {
software: 'yotsuba'
},
'4channel.org': {
- software: 'yotsuba'
+ canonical: '4chan.org'
},
'4cdn.org': {
- software: 'yotsuba'
+ canonical: '4chan.org'
}
},
init: function(cb) {
- var hostname;
+ var canonical, hostname;
$.extend(Conf['siteProperties'], Site.defaultProperties);
hostname = location.hostname;
while (hostname && !(hostname in Conf['siteProperties'])) {
hostname = hostname.replace(/^[^.]*\.?/, '');
}
- if (hostname && Conf['siteProperties'][hostname].software in SW) {
- this.set(hostname);
- cb();
+ if (hostname) {
+ if ((canonical = Conf['siteProperties'][hostname].canonical)) {
+ hostname = canonical;
+ }
+ if (Conf['siteProperties'][hostname].software in SW) {
+ this.set(hostname);
+ cb();
+ }
}
return $.onExists(doc, 'body', (function(_this) {
return function() {
@@ -7717,9 +7823,6 @@ Site = (function() {
this.hostname = hostname1;
this.properties = Conf['siteProperties'][this.hostname];
this.software = this.properties.software;
- if (this.software === 'yotsuba') {
- this.hostname = '4chan.org';
- }
return $.extend(this, SW[this.software]);
}
};
@@ -7860,7 +7963,9 @@ Redirect = (function() {
response: response
});
} else {
- CrossOrigin.json(url, load(i), true);
+ CrossOrigin.ajax(url, {
+ onloadend: load(i)
+ });
}
}
} else {
@@ -8025,15 +8130,13 @@ Filter = (function() {
filters: {},
results: {},
init: function() {
- var base, base1, boards, err, excludes, filter, hl, i, j, key, len, len1, line, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, sfwBoards, stub, top, type, types;
+ var base, base1, boards, err, excludes, file, filter, hide, hl, i, isstring, j, key, len, len1, line, mask, noti, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, regexp, stub, top, type, types;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
if (!Conf['Filtered Backlinks']) {
$.addClass(doc, 'hide-backlinks');
}
- nsfwBoards = BoardConfig.sfwBoards(false).join(',');
- sfwBoards = BoardConfig.sfwBoards(true).join(',');
for (key in Config.filter) {
ref1 = Conf[key].split('\n');
for (i = 0, len = ref1.length; i < len; i++) {
@@ -8041,16 +8144,13 @@ Filter = (function() {
if (line[0] === '#') {
continue;
}
- if (!(regexp = line.match(/\/(.+)\/(\w*)/))) {
+ if (!(regexp = line.match(/\/(.*)\/(\w*)/))) {
continue;
}
filter = line.replace(regexp[0], '');
- boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
- boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards);
- boards = boards === 'global' ? null : boards.split(',');
- excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase() : void 0) || null;
- excludes = excludes === null ? null : excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',');
- if (key === 'uniqueID' || key === 'MD5') {
+ boards = this.parseBoards((ref2 = filter.match(/(?:^|;)\s*boards:([^;]+)/)) != null ? ref2[1] : void 0);
+ excludes = this.parseBoards((ref3 = filter.match(/(?:^|;)\s*exclude:([^;]+)/)) != null ? ref3[1] : void 0);
+ if ((isstring = (key === 'uniqueID' || key === 'MD5'))) {
regexp = regexp[1];
} else {
try {
@@ -8061,10 +8161,19 @@ Filter = (function() {
continue;
}
}
- op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
+ op = ((ref4 = filter.match(/(?:^|;)\s*op:(no|only)/)) != null ? ref4[1] : void 0) || '';
+ mask = {
+ 'no': 1,
+ 'only': 2
+ }[op] || 0;
+ file = ((ref5 = filter.match(/(?:^|;)\s*file:(no|only)/)) != null ? ref5[1] : void 0) || '';
+ mask = mask | ({
+ 'no': 4,
+ 'only': 8
+ }[file] || 0);
stub = (function() {
- var ref5;
- switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
+ var ref6;
+ switch ((ref6 = filter.match(/(?:^|;)\s*stub:(yes|no)/)) != null ? ref6[1] : void 0) {
case 'yes':
return true;
case 'no':
@@ -8073,21 +8182,32 @@ Filter = (function() {
return Conf['Stubs'];
}
})();
- if (hl = /highlight/.test(filter)) {
- hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
- top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
+ noti = /(?:^|;)\s*notify/.test(filter);
+ if ((hl = /(?:^|;)\s*highlight/.test(filter))) {
+ hl = ((ref6 = filter.match(/(?:^|;)\s*highlight:([\w-]+)/)) != null ? ref6[1] : void 0) || 'filter-highlight';
+ top = ((ref7 = filter.match(/(?:^|;)\s*top:(yes|no)/)) != null ? ref7[1] : void 0) || 'yes';
top = top === 'yes';
}
if (key === 'general') {
if ((types = filter.match(/(?:^|;)\s*type:([^;]*)/))) {
- types = types[1].split(',').filter(function(x) {
- return x in Config.filter && x !== 'general';
- });
+ types = types[1].split(',');
} else {
types = ['subject', 'name', 'filename', 'comment'];
}
}
- filter = this.createFilter(regexp, boards, excludes, op, stub, hl, top);
+ hide = !(hl || noti);
+ filter = {
+ isstring: isstring,
+ regexp: regexp,
+ boards: boards,
+ excludes: excludes,
+ mask: mask,
+ hide: hide,
+ stub: stub,
+ hl: hl,
+ top: top,
+ noti: noti
+ };
if (key === 'general') {
for (j = 0, len1 = types.length; j < len1; j++) {
type = types[j];
@@ -8106,37 +8226,45 @@ Filter = (function() {
cb: this.node
});
},
- createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
- var settings, test;
- test = typeof regexp === 'string' ? function(value) {
- return regexp === value;
- } : function(value) {
- return regexp.test(value);
- };
- settings = {
- hide: !hl,
- stub: stub,
- "class": hl,
- top: top
- };
- return function(value, boardID, isReply) {
- if (boards && indexOf.call(boards, boardID) < 0) {
- return false;
+ parseBoards: function(boardsRaw) {
+ var boardID, boardID2, boards, i, j, len, len1, ref, ref1, ref2, ref3, ref4, siteFilter, siteID, siteProperties;
+ if (!boardsRaw) {
+ return false;
+ }
+ if ((boards = Filter.parseBoardsMemo[boardsRaw])) {
+ return boards;
+ }
+ boards = {};
+ siteFilter = '';
+ ref = boardsRaw.split(',');
+ for (i = 0, len = ref.length; i < len; i++) {
+ boardID = ref[i];
+ if (indexOf.call(boardID, ':') >= 0) {
+ ref1 = boardID.split(':').slice(-2), siteFilter = ref1[0], boardID = ref1[1];
}
- if (excludes && indexOf.call(excludes, boardID) >= 0) {
- return false;
+ ref2 = Conf['siteProperties'];
+ for (siteID in ref2) {
+ siteProperties = ref2[siteID];
+ if (siteProperties.canonical || siteID.slice(0, siteFilter.length) !== siteFilter) {
+ continue;
+ }
+ if (boardID === 'nsfw' || boardID === 'sfw') {
+ ref4 = ((ref3 = SW[siteProperties.software]) != null ? typeof ref3.sfwBoards === "function" ? ref3.sfwBoards(boardID === 'sfw') : void 0 : void 0) || [];
+ for (j = 0, len1 = ref4.length; j < len1; j++) {
+ boardID2 = ref4[j];
+ boards[siteID + "/" + boardID2] = true;
+ }
+ } else {
+ boards[siteID + "/" + (encodeURIComponent(boardID))] = true;
+ }
}
- if (isReply && op === 'only' || !isReply && op === 'no') {
- return false;
- }
- if (!test(value)) {
- return false;
- }
- return settings;
- };
+ }
+ Filter.parseBoardsMemo[boardsRaw] = boards;
+ return boards;
},
+ parseBoardsMemo: {},
test: function(post, hideable) {
- var filter, hide, hl, i, key, len, ref, ref1, result, stub, top, value;
+ var board, filter, hide, hl, i, key, len, mask, noti, ref, ref1, site, stub, top, value;
if (hideable == null) {
hideable = true;
}
@@ -8147,25 +8275,34 @@ Filter = (function() {
stub = true;
hl = void 0;
top = false;
+ noti = false;
if (QuoteYou.isYou(post)) {
hideable = false;
}
+ mask = (post.isReply ? 2 : 1);
+ mask = mask | (post.file ? 4 : 8);
+ board = post.siteID + "/" + post.boardID;
+ site = post.siteID + "/*";
for (key in Filter.filters) {
- if (((value = Filter[key](post)) != null)) {
+ if (((value = Filter.value(key, post)) != null)) {
ref = Filter.filters[key];
for (i = 0, len = ref.length; i < len; i++) {
filter = ref[i];
- if ((result = filter(value, post.boardID, post.isReply))) {
- if (result.hide) {
- if (hideable) {
- hide = true;
- stub && (stub = result.stub);
- }
- } else {
- if (!(hl && (ref1 = result["class"], indexOf.call(hl, ref1) >= 0))) {
- (hl || (hl = [])).push(result["class"]);
- }
- top || (top = result.top);
+ if ((filter.boards && !(filter.boards[board] || filter.boards[site])) || (filter.excludes && (filter.excludes[board] || filter.excludes[site])) || (filter.mask & mask) || (filter.isstring ? filter.regexp !== value : !filter.regexp.test(value))) {
+ continue;
+ }
+ if (filter.hide) {
+ if (hideable) {
+ hide = true;
+ stub && (stub = filter.stub);
+ }
+ } else {
+ if (!(hl && (ref1 = filter.hl, indexOf.call(hl, ref1) >= 0))) {
+ (hl || (hl = [])).push(filter.hl);
+ }
+ top || (top = filter.top);
+ if (filter.noti) {
+ noti = true;
}
}
}
@@ -8179,16 +8316,17 @@ Filter = (function() {
} else {
return {
hl: hl,
- top: top
+ top: top,
+ noti: noti
};
}
},
node: function() {
- var hide, hl, ref, stub, top;
+ var hide, hl, noti, ref, stub, top;
if (this.isClone) {
return;
}
- ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top;
+ ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top, noti = ref.noti;
if (hide) {
if (this.isReply) {
PostHiding.hide(this, stub);
@@ -8201,53 +8339,71 @@ Filter = (function() {
$.addClass.apply($, [this.nodes.root].concat(slice.call(hl)));
}
}
+ if (noti && Unread.posts && (this.ID > Unread.lastReadPost) && !QuoteYou.isYou(this)) {
+ return Unread.openNotification(this, ' triggered a notification filter');
+ }
},
isHidden: function(post) {
return !!Filter.test(post).hide;
},
- postID: function(post) {
- return "" + post.ID;
+ valueF: {
+ postID: function(post) {
+ return "" + post.ID;
+ },
+ name: function(post) {
+ return post.info.name;
+ },
+ uniqueID: function(post) {
+ return post.info.uniqueID || '';
+ },
+ tripcode: function(post) {
+ return post.info.tripcode;
+ },
+ capcode: function(post) {
+ return post.info.capcode;
+ },
+ pass: function(post) {
+ return post.info.pass;
+ },
+ email: function(post) {
+ return post.info.email;
+ },
+ subject: function(post) {
+ return post.info.subject || (post.isReply ? void 0 : '');
+ },
+ comment: function(post) {
+ var base;
+ return (base = post.info).comment != null ? base.comment : base.comment = Build.parseComment(post.info.commentHTML.innerHTML);
+ },
+ flag: function(post) {
+ return post.info.flag;
+ },
+ filename: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.name : void 0;
+ },
+ dimensions: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.dimensions : void 0;
+ },
+ filesize: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.size : void 0;
+ },
+ MD5: function(post) {
+ var ref;
+ return (ref = post.file) != null ? ref.MD5 : void 0;
+ }
},
- name: function(post) {
- return post.info.name;
- },
- uniqueID: function(post) {
- return post.info.uniqueID;
- },
- tripcode: function(post) {
- return post.info.tripcode;
- },
- capcode: function(post) {
- return post.info.capcode;
- },
- pass: function(post) {
- return post.info.pass;
- },
- subject: function(post) {
- return post.info.subject || (post.isReply ? void 0 : '');
- },
- comment: function(post) {
- var base;
- return (base = post.info).comment != null ? base.comment : base.comment = Build.parseComment(post.info.commentHTML.innerHTML);
- },
- flag: function(post) {
- return post.info.flag;
- },
- filename: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.name : void 0;
- },
- dimensions: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.dimensions : void 0;
- },
- filesize: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.size : void 0;
- },
- MD5: function(post) {
- var ref;
- return (ref = post.file) != null ? ref.MD5 : void 0;
+ value: function(key, post) {
+ if (key in Filter.valueF) {
+ return Filter.valueF[key](post);
+ } else {
+ return key.split('+').map(function(k) {
+ var base;
+ return (typeof (base = Filter.valueF)[k] === "function" ? base[k](post) : void 0) || '';
+ }).join('\n');
+ }
},
addFilter: function(type, re, cb) {
return $.get(type, Conf[type], function(item) {
@@ -8303,7 +8459,7 @@ Filter = (function() {
},
subEntries: []
};
- ref1 = [['Name', 'name'], ['Unique ID', 'uniqueID'], ['Tripcode', 'tripcode'], ['Capcode', 'capcode'], ['Pass Date', 'pass'], ['Subject', 'subject'], ['Comment', 'comment'], ['Flag', 'flag'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'MD5']];
+ ref1 = [['Name', 'name'], ['Unique ID', 'uniqueID'], ['Tripcode', 'tripcode'], ['Capcode', 'capcode'], ['Pass Date', 'pass'], ['Email', 'email'], ['Subject', 'subject'], ['Comment', 'comment'], ['Flag', 'flag'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'MD5']];
for (i = 0, len = ref1.length; i < len; i++) {
type = ref1[i];
entry.subEntries.push(Filter.menu.createSubEntry(type[0], type[1]));
@@ -8322,7 +8478,7 @@ Filter = (function() {
el: el,
open: function(post) {
var value;
- value = Filter[type](post);
+ value = Filter.value(type, post);
return value != null;
}
};
@@ -8330,7 +8486,7 @@ Filter = (function() {
makeFilter: function() {
var re, type, value;
type = this.dataset.type;
- value = Filter[type](Filter.menu.post);
+ value = Filter.value(type, Filter.menu.post);
re = type === 'uniqueID' || type === 'MD5' ? value : Filter.escape(value);
re = type === 'uniqueID' || type === 'MD5' ? "/" + re + "/" : "/^" + re + "$/";
return Filter.addFilter(type, re, function() {
@@ -9137,6 +9293,11 @@ BoardConfig = (function() {
domain: function(board) {
return "boards." + (BoardConfig.isSFW(board) ? '4channel' : '4chan') + ".org";
},
+ isArchived: function(board) {
+ var data;
+ data = (this.boards || Conf['boardConfig'].boards)[board];
+ return !data || data.is_archived;
+ },
noAudio: function(boardID) {
var boards;
if (Site.software !== 'yotsuba') {
@@ -9198,25 +9359,30 @@ Build = (function() {
sameThread: function(boardID, threadID) {
return g.VIEW === 'thread' && g.BOARD.ID === boardID && g.THREADID === +threadID;
},
- postURL: function(boardID, threadID, postID) {
- if (Build.sameThread(boardID, threadID)) {
- return "#p" + postID;
+ threadURL: function(boardID, threadID) {
+ if (boardID !== g.BOARD.ID) {
+ return "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/thread/" + threadID;
+ } else if (g.VIEW !== 'thread' || +threadID !== g.THREADID) {
+ return "/" + boardID + "/thread/" + threadID;
} else {
- return "/" + boardID + "/thread/" + threadID + "#p" + postID;
+ return '';
}
},
- parseJSON: function(data, boardID) {
- var o;
+ postURL: function(boardID, threadID, postID) {
+ return (Build.threadURL(boardID, threadID)) + "#p" + postID;
+ },
+ parseJSON: function(data, boardID, siteID) {
+ var key, o;
o = {
ID: data.no,
threadID: data.resto || data.no,
boardID: boardID,
+ siteID: siteID || Site.hostname,
isReply: !!data.resto,
isSticky: !!data.sticky,
isClosed: !!data.closed,
isArchived: !!data.archived,
- fileDeleted: !!data.filedeleted,
- xa18: data.xa18
+ fileDeleted: !!data.filedeleted
};
o.info = {
subject: Build.unescape(data.sub),
@@ -9260,6 +9426,11 @@ Build = (function() {
o.file.dimensions = o.file.width + "x" + o.file.height;
}
}
+ for (key in data) {
+ if (key[0] === 'x') {
+ o[key] = data[key];
+ }
+ }
return o;
},
parseComment: function(html) {
@@ -9282,7 +9453,7 @@ Build = (function() {
return Build.post(o);
},
post: function(o) {
- var ID, boardID, capcode, capcodeDescription, capcodeLC, capcodeLong, capcodePlural, commentHTML, container, dateText, dateUTC, email, file, fileBlock, fileThumb, fileURL, flag, flagCode, flagCodeTroll, gifIcon, href, i, len, match, name, pass, postClass, postInfo, postLink, protocol, quote, quoteLink, ref, ref1, shortFilename, staticPath, subject, threadID, tripcode, uniqueID, wholePost;
+ var ID, boardID, capcode, capcodeDescription, capcodeLC, capcodeLong, capcodePlural, commentHTML, container, dateText, dateUTC, email, file, fileBlock, fileThumb, fileURL, flag, flagCode, flagCodeTroll, gifIcon, href, i, len, match, name, pass, postClass, postInfo, postLink, protocol, quote, quoteLink, ref, ref1, shortFilename, staticPath, subject, threadID, tripcode, uniqueID, url, wholePost;
ID = o.ID, threadID = o.threadID, boardID = o.boardID, file = o.file;
ref = o.info, subject = ref.subject, email = ref.email, name = ref.name, tripcode = ref.tripcode, capcode = ref.capcode, pass = ref.pass, uniqueID = ref.uniqueID, flagCode = ref.flagCode, flagCodeTroll = ref.flagCodeTroll, flag = ref.flag, dateUTC = ref.dateUTC, dateText = ref.dateText, commentHTML = ref.commentHTML;
staticPath = Build.staticPath, gifIcon = Build.gifIcon;
@@ -9305,10 +9476,11 @@ Build = (function() {
capcodeDescription = "a 4chan " + capcodeLong;
}
}
- postLink = Build.postURL(boardID, threadID, ID);
- quoteLink = Build.sameThread(boardID, threadID) ? "javascript:quote('" + (+ID) + "');" : "/" + boardID + "/thread/" + threadID + "#q" + ID;
+ url = Build.threadURL(boardID, threadID);
+ postLink = url + "#p" + ID;
+ quoteLink = Build.sameThread(boardID, threadID) ? "javascript:quote('" + (+ID) + "');" : url + "#q" + ID;
postInfo = {
- innerHTML: ""
+ innerHTML: ""
};
/* File Info */
@@ -9336,12 +9508,14 @@ Build = (function() {
for (i = 0, len = ref1.length; i < len; i++) {
quote = ref1[i];
href = quote.getAttribute('href');
- if ((href[0] === '#') && !(Build.sameThread(boardID, threadID))) {
- quote.href = ("/" + boardID + "/thread/" + threadID) + href;
- } else if ((match = href.match(/^\/([^\/]+)\/thread\/(\d+)/)) && (Build.sameThread(match[1], match[2]))) {
- quote.href = href.match(/(#[^#]*)?$/)[0] || '#';
- } else if (/^\d+(#|$)/.test(href) && !(g.VIEW === 'thread' && g.BOARD.ID === boardID)) {
- quote.href = "/" + boardID + "/thread/" + href;
+ if (href[0] === '#') {
+ if (!Build.sameThread(boardID, threadID)) {
+ quote.href = Build.threadURL(boardID, threadID) + href;
+ }
+ } else {
+ if ((match = quote.href.match(SW.yotsuba.regexp.quotelink)) && (Build.sameThread(match[1], match[2]))) {
+ quote.href = href.match(/(#[^#]*)?$/)[0] || '#';
+ }
}
}
return container;
@@ -9904,7 +10078,7 @@ Header = (function() {
}
}
if (/-expired/.test(t)) {
- if (boardID !== 'b' && boardID !== 'f' && boardID !== 'trash' && boardID !== 'bant') {
+ if (BoardConfig.isArchived(boardID)) {
a.href = "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/archive";
} else {
return a.firstChild;
@@ -10230,7 +10404,7 @@ Index = (function() {
showHiddenThreads: false,
changed: {},
init: function() {
- var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, select, sortEntry, tRaw, watchSettings;
+ var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, select, sortEntry, tRaw, watchSettings;
if (!(g.VIEW === 'index' && g.BOARD.ID !== 'f')) {
return;
}
@@ -10320,7 +10494,7 @@ Index = (function() {
innerHTML: "Index Catalog Archive Bottom ... × — [Show ] Index Sort Bump order Last reply Last long reply Creation date Reply count File count Image Size Small Large Index Mode Paged Infinite scrolling All threads Catalog "
});
$('.cataloglink a', this.navLinks).href = CatalogLinks.catalog();
- if ((ref5 = g.BOARD.ID) === 'b' || ref5 === 'trash' || ref5 === 'bant') {
+ if (!BoardConfig.isArchived(g.BOARD.ID)) {
$('.archlistlink', this.navLinks).hidden = true;
}
$.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront);
@@ -10339,9 +10513,9 @@ Index = (function() {
$.on(this.selectSort, 'change', this.cb.sort);
$.on(this.selectSize, 'change', $.cb.value);
$.on(this.selectSize, 'change', this.cb.size);
- ref6 = [this.selectMode, this.selectSize];
- for (k = 0, len1 = ref6.length; k < len1; k++) {
- select = ref6[k];
+ ref5 = [this.selectMode, this.selectSize];
+ for (k = 0, len1 = ref5.length; k < len1; k++) {
+ select = ref5[k];
select.value = Conf[select.name];
}
this.selectRev.checked = /-rev$/.test(Index.currentSort);
@@ -10350,12 +10524,12 @@ Index = (function() {
this.lastLongInputs = $$('input', this.lastLongOptions);
this.lastLongThresholds = [0, 0];
this.lastLongOptions.hidden = this.selectSort.value !== 'lastlong';
- ref7 = this.lastLongInputs;
- for (i = l = 0, len2 = ref7.length; l < len2; i = ++l) {
- input = ref7[i];
+ ref6 = this.lastLongInputs;
+ for (i = l = 0, len2 = ref6.length; l < len2; i = ++l) {
+ input = ref6[i];
$.on(input, 'change', this.cb.lastLongThresholds);
tRaw = Conf["Last Long Reply Thresholds " + i];
- input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref8 = tRaw[g.BOARD.ID]) != null ? ref8 : 100 : tRaw;
+ input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref7 = tRaw[g.BOARD.ID]) != null ? ref7 : 100 : tRaw;
}
this.root = $.el('div', {
className: 'board json-index'
@@ -10376,7 +10550,7 @@ Index = (function() {
return d.title = d.title.replace(/\ -\ Page\ \d+/, '');
});
$.onExists(doc, '.board > .thread > .postContainer, .board + *', function() {
- var board, el, len3, m, ref9, timeEl, topNavPos;
+ var board, el, len3, m, ref8, timeEl, topNavPos;
Build.hat = $('.board > .thread > img:first-child');
if (Build.hat) {
g.BOARD.threads.forEach(function(thread) {
@@ -10395,9 +10569,9 @@ Index = (function() {
try {
d.implementation.createDocument(null, null, null).appendChild(board);
} catch (_error) {}
- ref9 = $$('.navLinks');
- for (m = 0, len3 = ref9.length; m < len3; m++) {
- el = ref9[m];
+ ref8 = $$('.navLinks');
+ for (m = 0, len3 = ref8.length; m < len3; m++) {
+ el = ref8[m];
$.rm(el);
}
$.rm($.id('ctrl-top'));
@@ -10969,56 +11143,42 @@ Index = (function() {
return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? '1 hidden thread' : hiddenCount + " hidden threads";
},
update: function(firstTime) {
- var now, ref, ref1;
- if ((ref = Index.req) != null) {
- ref.abort();
+ var oldReq;
+ if ((oldReq = Index.req)) {
+ delete Index.req;
+ oldReq.abort();
}
- if ((ref1 = Index.notice) != null) {
- ref1.close();
- }
- if (Conf['Index Refresh Notifications'] && d.readyState !== 'loading') {
- Index.notice = new Notice('info', 'Refreshing index...');
+ if (Conf['Index Refresh Notifications']) {
+ Index.notice || (Index.notice = new Notice('info', 'Refreshing index...'));
} else {
- now = Date.now();
- $.ready(function() {
- return Index.nTimeout = setTimeout((function() {
- if (Index.req && !Index.notice) {
- return Index.notice = new Notice('info', 'Refreshing index...');
- }
- }), 3 * $.SECOND - (Date.now() - now));
- });
+ Index.nTimeout || (Index.nTimeout = setTimeout(function() {
+ return Index.notice || (Index.notice = new Notice('info', 'Refreshing index...'));
+ }, 3 * $.SECOND));
}
if (!firstTime && d.readyState !== 'loading' && !$('.board + *')) {
location.reload();
return;
}
- Index.req = $.ajax(location.protocol + "//a.4cdn.org/" + g.BOARD + "/catalog.json", {
- onabort: Index.load,
- onloadend: Index.load
- }, {
- whenModified: 'Index'
- });
+ Index.req = $.whenModified(Site.urls.catalogJSON({
+ boardID: g.BOARD.ID
+ }), 'Index', Index.load);
return $.addClass(Index.button, 'fa-spin');
},
- load: function(e) {
- var err, nTimeout, notice, ref, req, timeEl;
+ load: function() {
+ var err, nTimeout, notice, ref, timeEl;
+ if (this !== Index.req) {
+ return;
+ }
$.rmClass(Index.button, 'fa-spin');
- req = Index.req, notice = Index.notice, nTimeout = Index.nTimeout;
+ notice = Index.notice, nTimeout = Index.nTimeout;
if (nTimeout) {
clearTimeout(nTimeout);
}
delete Index.nTimeout;
delete Index.req;
delete Index.notice;
- if (e.type === 'abort') {
- req.onloadend = null;
- if (notice != null) {
- notice.close();
- }
- return;
- }
- if ((ref = req.status) !== 200 && ref !== 304) {
- err = "Index refresh failed. " + (req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error');
+ if ((ref = this.status) !== 200 && ref !== 304) {
+ err = "Index refresh failed. " + (this.status ? "Error " + this.statusText + " (" + this.status + ")" : 'Connection Error');
if (notice) {
notice.setType('warning');
notice.el.lastElementChild.textContent = err;
@@ -11029,9 +11189,9 @@ Index = (function() {
return;
}
try {
- if (req.status === 200) {
- Index.parse(req.response);
- } else if (req.status === 304) {
+ if (this.status === 200) {
+ Index.parse(this.response);
+ } else if (this.status === 304) {
Index.pageLoad();
}
} catch (_error) {
@@ -11056,7 +11216,7 @@ Index = (function() {
}
}
timeEl = $('#index-last-refresh time', Index.navLinks);
- timeEl.dataset.utc = Date.parse(req.getResponseHeader('Last-Modified'));
+ timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified'));
return RelativeDates.update(timeEl);
},
parse: function(pages) {
@@ -11909,20 +12069,27 @@ Settings = (function() {
return $.after($('input[name="Stubs"]', section).parentNode.parentNode, div);
},
"export": function() {
- return $.get(Conf, function(Conf) {
- delete Conf['boardConfig'];
+ var Conf2;
+ Conf2 = {};
+ $.extend(Conf2, Conf);
+ return $.get(Conf2, function(Conf2) {
+ delete Conf2['boardConfig'];
return Settings.downloadExport({
version: g.VERSION,
date: Date.now(),
- Conf: Conf
+ Conf: Conf2
});
});
},
downloadExport: function(data) {
- var a, p;
+ var a, blob, p, url;
+ blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: 'application/json'
+ });
+ url = URL.createObjectURL(blob);
a = $.el('a', {
download: "4chan X v" + g.VERSION + "-" + data.date + ".json",
- href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2)))))
+ href: url
});
p = $('.imp-exp-result', Settings.dialog);
$.rmAll(p);
@@ -12352,6 +12519,11 @@ Settings = (function() {
set('siteProperties', siteProperties);
}
}
+ if (compareString < '00001.00014.00006.00006') {
+ if (data['sauces'] != null) {
+ set('sauces', data['sauces'].replace(/\/\/%\$1\.deviantart\.com\/gallery\/#\/d%\$2;regexp:\/\^\\w\+_by_\(\\w\+\)-d\(\[\\da-z\]\+\)\//g, '//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/'));
+ }
+ }
return changes;
},
loadSettings: function(data, cb) {
@@ -12381,7 +12553,7 @@ Settings = (function() {
filter: function(section) {
var select;
$.extend(section, {
- innerHTML: "Guide General Post number Name Unique ID Tripcode Capcode Pass Date Subject Comment Flag Filename Image dimensions Filesize Image MD5
"
+ innerHTML: "Guide General Post number Name Unique ID Tripcode Capcode Pass Date Email Subject Comment Flag Filename Image dimensions Filesize Image MD5
"
});
select = $('select', section);
$.on(select, 'change', Settings.selectFilter);
@@ -12412,7 +12584,7 @@ Settings = (function() {
};
});
$.extend(div, {
- innerHTML: "Filter is disabled.
Use regular expressions , one per line. Lines starting with a # will be ignored. For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive. MD5 filtering uses exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:Per boards, separate them with commas. It is global if not specified. Use sfw and nsfw to reference all worksafe or not-worksafe boards. For example: boards:a,jp;. In case of a global rule or one that uses sfw/nsfw, select boards to be excluded from the filter. For example: exclude:vg,v;. Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default). For example: op:only;, op:no; or op:yes;. Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`). For example: stub:yes; or stub:no;. Highlight instead of hiding. You can specify a class name to use with a userstyle. For example: highlight; or highlight:wallpaper;. Highlighted OPs will have their threads put on top of the board index by default. For example: top:yes; or top:no;. Filters in the \"General\" section apply to multiple fields, by default subject,name,filename,comment. The fields can be specified with the type option, separated by commas. For example: type:" + E.cat(filterTypes) + ";. Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there. The native catalog has its own separate filter list.
"
+ innerHTML: "Filter is disabled.
Use regular expressions , one per line. Lines starting with a # will be ignored. For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive. MD5 and Unique ID filtering use exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:Per boards, separate them with commas. It is global if not specified. Use sfw and nsfw to reference all worksafe or not-worksafe boards. For example: boards:a,jp;. To specify boards on a particular site, put the beginning of the domain and a slash character before the list. Any initial www. should not be included, and all 4chan domains are considered 4chan.org. For example: boards:4:a,jp,sama:a,z;. An asterisk can be used to specify all boards on a site. For example: boards:4:*;. Select boards to be excluded from the filter. The syntax is the same as for the boards: option above. For example: exclude:vg,v;. Filter OPs only along with their threads (\`only\`) or replies only (\`no\`). For example: op:only; or op:no;. Filter only posts with files (\`only\`) or only posts without files (\`no\`). For example: file:only; or file:no;. Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`). For example: stub:yes; or stub:no;. Highlight instead of hiding. You can specify a class name to use with a userstyle. For example: highlight; or highlight:wallpaper;. Highlighted OPs will have their threads put on top of the board index by default. For example: top:yes; or top:no;. Show a desktop notification instead of hiding. For example: notify;. Filters in the \"General\" section apply to multiple fields, by default subject,name,filename,comment. The fields can be specified with the type option, separated by commas. For example: type:" + E.cat(filterTypes) + ";. Types can also be combined with a + sign; this indicates the filter applies to the given fields joined by newlines. For example: type:filename+filesize+dimensions;. Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there. The native catalog has its own separate filter list.
"
});
return $('.warning', div).hidden = Conf['Filter'];
},
@@ -13119,8 +13291,9 @@ UI = (function() {
};
hoverstart = function(arg) {
- var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
- root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
+ var cb, el, endEvents, height, latestEvent, noRemove, o, rect, ref, root, width;
+ root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, width = arg.width, cb = arg.cb, noRemove = arg.noRemove;
+ rect = root.getBoundingClientRect();
o = {
root: root,
el: el,
@@ -13132,7 +13305,10 @@ UI = (function() {
clientHeight: doc.clientHeight,
clientWidth: doc.clientWidth,
height: height,
- noRemove: noRemove
+ width: width,
+ noRemove: noRemove,
+ clientX: (rect.left + rect.right) / 2,
+ clientY: (rect.top + rect.bottom) / 2
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
@@ -13160,16 +13336,22 @@ UI = (function() {
hoverstart.padding = 25;
hover = function(e) {
- var clientX, clientY, height, left, ref, right, style, threshold, top;
+ var clientX, clientY, height, left, marginX, ref, ref1, right, style, threshold, top, width;
this.latestEvent = e;
height = (this.height || this.el.offsetHeight) + hoverstart.padding;
- clientX = e.clientX, clientY = e.clientY;
+ width = this.width || this.el.offsetWidth;
+ ref = Conf['Follow Cursor'] ? e : this, clientX = ref.clientX, clientY = ref.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
if (!this.isImage) {
threshold = Math.max(threshold, this.clientWidth - 400);
}
- ref = clientX <= threshold ? [clientX + 45 + 'px', ''] : ['', this.clientWidth - clientX + 45 + 'px'], left = ref[0], right = ref[1];
+ marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45;
+ if (this.isImage) {
+ marginX = Math.min(marginX, this.clientWidth - width);
+ }
+ marginX += 'px';
+ ref1 = clientX <= threshold ? [marginX, ''] : ['', marginX], left = ref1[0], right = ref1[1];
style = this.style;
style.top = top + 'px';
style.left = left;
@@ -13878,8 +14060,11 @@ ImageCommon = (function() {
return cb(URL);
}
};
- return $.ajax(location.protocol + "//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", {
- onload: function() {
+ return $.ajax(Site.urls.threadJSON({
+ boardID: post.boardID,
+ threadID: post.threadID
+ }), {
+ onloadend: function() {
var i, len, postObj, ref;
if (this.status === 404) {
post.kill(!post.isClone);
@@ -14272,7 +14457,7 @@ ImageExpand = (function() {
}
},
mouseout: function(e) {
- if (mousedown && e.clientX <= this.getBoundingClientRect().left) {
+ if (((e.buttons & 1) || mousedown) && e.clientX <= this.getBoundingClientRect().left) {
return ImageExpand.toggle(Get.postFromNode(this));
}
}
@@ -14453,7 +14638,7 @@ ImageHover = (function() {
},
mouseover: function(post) {
return function(e) {
- var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
+ var el, error, file, height, isVideo, maxHeight, maxWidth, ref, ref1, scale, width, x;
if (!doc.contains(this)) {
return;
}
@@ -14489,28 +14674,32 @@ ImageHover = (function() {
}
}
}
- ref1 = (function() {
- var i, len, ref1, results;
- ref1 = file.dimensions.split('x');
- results = [];
- for (i = 0, len = ref1.length; i < len; i++) {
- x = ref1[i];
- results.push(+x);
- }
- return results;
- })(), width = ref1[0], height = ref1[1];
- ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
- maxWidth = Math.max(left, doc.clientWidth - right);
- maxHeight = doc.clientHeight - UI.hover.padding;
- scale = Math.min(1, maxWidth / width, maxHeight / height);
- el.style.maxWidth = (scale * width) + "px";
- el.style.maxHeight = (scale * height) + "px";
+ if (file.dimensions) {
+ ref1 = (function() {
+ var i, len, ref1, results;
+ ref1 = file.dimensions.split('x');
+ results = [];
+ for (i = 0, len = ref1.length; i < len; i++) {
+ x = ref1[i];
+ results.push(+x);
+ }
+ return results;
+ })(), width = ref1[0], height = ref1[1];
+ maxWidth = doc.clientWidth;
+ maxHeight = doc.clientHeight - UI.hover.padding;
+ scale = Math.min(1, maxWidth / width, maxHeight / height);
+ width *= scale;
+ height *= scale;
+ el.style.maxWidth = width + "px";
+ el.style.maxHeight = height + "px";
+ }
return UI.hover({
root: this,
el: el,
latestEvent: e,
endEvents: 'mouseout click',
- height: scale * height,
+ height: height,
+ width: width,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@@ -14918,7 +15107,7 @@ Sauce = (function() {
if (!matches) {
return orig;
}
- type = matches[parameter.slice(1)];
+ type = matches[parameter.slice(1)] || '';
} else {
type = Sauce.formatters[parameter](post, ext);
if (type == null) {
@@ -15368,7 +15557,7 @@ Embedding = (function() {
return Embedding.flushTitles(service);
}
} else {
- return CrossOrigin.json(service.api(uid), (function() {
+ return CrossOrigin.cache(service.api(uid), (function() {
return Embedding.cb.title(this, data);
}));
}
@@ -15387,7 +15576,7 @@ Embedding = (function() {
Embedding.cb.title(this, data);
}
};
- return CrossOrigin.json(service.api((function() {
+ return CrossOrigin.cache(service.api((function() {
var j, len, results;
results = [];
for (j = 0, len = queue.length; j < len; j++) {
@@ -15609,7 +15798,7 @@ Embedding = (function() {
hidden: true,
id: "gist-embed-" + (counter++)
});
- CrossOrigin.json("https://api.github.com/gists/" + a.dataset.uid, function() {
+ CrossOrigin.cache("https://api.github.com/gists/" + a.dataset.uid, function() {
el.textContent = Object.values(this.response.files)[0].content;
el.className = 'prettyprint';
$.global(function() {
@@ -16165,7 +16354,7 @@ ArchiveLink = (function() {
} : function(post) {
var typeParam, value;
typeParam = type === 'country' && post.info.flagCodeTroll ? 'tag' : type;
- value = type === 'country' ? post.info.flagCode || post.info.flagCodeTroll : Filter[type](post);
+ value = type === 'country' ? post.info.flagCode || post.info.flagCodeTroll : Filter.value(type, post);
if (!value) {
return false;
}
@@ -16340,18 +16529,21 @@ DeleteLink = (function() {
return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), {
responseType: 'document',
withCredentials: true,
- onload: function() {
+ onloadend: function() {
return DeleteLink.load(link, post, fileOnly, this.response);
},
- onerror: function() {
- return DeleteLink.error(link, post);
- }
- }, {
form: $.formData(form)
});
},
load: function(link, post, fileOnly, resDoc) {
var el, msg;
+ if (!resDoc) {
+ new Notice('warning', 'Connection error, please retry.', 20);
+ if (post.fullID === DeleteLink.post.fullID) {
+ $.on(link, 'click', DeleteLink.toggle);
+ }
+ return;
+ }
link.textContent = DeleteLink.linkText(fileOnly);
if (resDoc.title === '4chan - Banned') {
el = $.el('span', {
@@ -16380,12 +16572,6 @@ DeleteLink = (function() {
}
}
},
- error: function(link, post) {
- new Notice('warning', 'Connection error, please retry.', 20);
- if (post.fullID === DeleteLink.post.fullID) {
- return $.on(link, 'click', DeleteLink.toggle);
- }
- },
cooldown: {
seconds: {},
start: function(post, seconds) {
@@ -16880,7 +17066,7 @@ CatalogLinks = (function() {
if (board == null) {
board = g.BOARD.ID;
}
- if (Conf['External Catalog'] && (board === 'a' || board === 'c' || board === 'g' || board === 'biz' || board === 'k' || board === 'm' || board === 'o' || board === 'p' || board === 'v' || board === 'vg' || board === 'vr' || board === 'w' || board === 'wg' || board === 'cm' || board === '3' || board === 'adv' || board === 'an' || board === 'asp' || board === 'cgl' || board === 'ck' || board === 'co' || board === 'diy' || board === 'fa' || board === 'fit' || board === 'gd' || board === 'int' || board === 'jp' || board === 'lit' || board === 'mlp' || board === 'mu' || board === 'n' || board === 'out' || board === 'po' || board === 'sci' || board === 'sp' || board === 'tg' || board === 'toy' || board === 'trv' || board === 'tv' || board === 'vp' || board === 'wsg' || board === 'x' || board === 'f' || board === 'pol' || board === 's4s' || board === 'lgbt')) {
+ if (Conf['External Catalog'] && (board === '3' || board === 'a' || board === 'adv' || board === 'an' || board === 'asp' || board === 'biz' || board === 'c' || board === 'cgl' || board === 'ck' || board === 'cm' || board === 'co' || board === 'diy' || board === 'f' || board === 'fa' || board === 'fit' || board === 'g' || board === 'gd' || board === 'his' || board === 'i' || board === 'int' || board === 'jp' || board === 'k' || board === 'lgbt' || board === 'lit' || board === 'm' || board === 'mlp' || board === 'mu' || board === 'n' || board === 'news' || board === 'o' || board === 'out' || board === 'p' || board === 'po' || board === 'pol' || board === 's4s' || board === 'sci' || board === 'sp' || board === 'tg' || board === 'toy' || board === 'trv' || board === 'tv' || board === 'v' || board === 'vg' || board === 'vip' || board === 'vp' || board === 'vr' || board === 'w' || board === 'wg' || board === 'wsg' || board === 'wsr' || board === 'x')) {
return "//catalog.neet.tv/" + board + "/";
} else if (Conf['JSON Index'] && Conf['Use 4chan X Catalog']) {
if (((ref = location.hostname) === 'boards.4chan.org' || ref === 'boards.4channel.org') && g.BOARD.ID === board && g.VIEW === 'index') {
@@ -16952,12 +17138,6 @@ ExpandComment = (function() {
if (g.VIEW !== 'index' || !Conf['Comment Expansion'] || Conf['JSON Index']) {
return;
}
- if (g.BOARD.ID === 'g') {
- this.callbacks.push(Fourchan.code);
- }
- if (g.BOARD.ID === 'sci') {
- this.callbacks.push(Fourchan.math);
- }
return Callbacks.Post.push({
name: 'Comment Expansion',
cb: this.node
@@ -16985,7 +17165,10 @@ ExpandComment = (function() {
return;
}
a.textContent = "Post No." + post + " Loading...";
- return $.cache(location.protocol + "//a.4cdn.org" + (a.pathname.split(/\/+/).splice(0, 4).join('/')) + ".json", function() {
+ return $.cache(Site.urls.threadJSON({
+ boardID: post.boardID,
+ threadID: post.threadID
+ }), function() {
return ExpandComment.parse(this, a, post);
});
},
@@ -17003,7 +17186,7 @@ ExpandComment = (function() {
var callback, clone, comment, href, i, j, k, len, len1, len2, postObj, posts, quote, ref, ref1, spoilerRange, status;
status = req.status;
if (status !== 200 && status !== 304) {
- a.textContent = "Error " + req.statusText + " (" + status + ")";
+ a.textContent = status ? "Error " + req.statusText + " (" + status + ")" : 'Connection Error';
return;
}
posts = req.response.posts;
@@ -17084,15 +17267,16 @@ ExpandThread = (function() {
return $.on(a, 'click', ExpandThread.cbToggle);
},
disconnect: function(refresh) {
- var ref, ref1, status, threadID;
+ var oldReq, ref, status, threadID;
if (g.VIEW === 'thread' || !Conf['Thread Expansion']) {
return;
}
ref = ExpandThread.statuses;
for (threadID in ref) {
status = ref[threadID];
- if ((ref1 = status.req) != null) {
- ref1.abort();
+ if ((oldReq = status.req)) {
+ delete status.req;
+ oldReq.abort();
}
delete ExpandThread.statuses[threadID];
}
@@ -17140,17 +17324,24 @@ ExpandThread = (function() {
var status;
ExpandThread.statuses[thread] = status = {};
a.textContent = Build.summaryText.apply(Build, ['...'].concat(slice.call(a.textContent.match(/\d+/g))));
- return status.req = $.cache(location.protocol + "//a.4cdn.org/" + thread.board + "/thread/" + thread + ".json", function() {
+ return status.req = $.cache(Site.urls.threadJSON({
+ boardID: thread.board.ID,
+ threadID: thread.ID
+ }), function() {
+ if (this !== status.req) {
+ return;
+ }
delete status.req;
return ExpandThread.parse(this, thread, a);
});
},
contract: function(thread, a, threadRoot) {
- var filesCount, i, inlined, len, num, postsCount, replies, reply, status;
+ var filesCount, i, inlined, len, num, oldReq, postsCount, replies, reply, status;
status = ExpandThread.statuses[thread];
delete ExpandThread.statuses[thread];
- if (status.req) {
- status.req.abort();
+ if ((oldReq = status.req)) {
+ delete status.req;
+ oldReq.abort();
if (a) {
a.textContent = Build.summaryText.apply(Build, ['+'].concat(slice.call(a.textContent.match(/\d+/g))));
}
@@ -17200,7 +17391,7 @@ ExpandThread = (function() {
parse: function(req, thread, a) {
var a2, filesCount, i, len, post, postData, posts, postsCount, postsRoot, ref, ref1, root;
if ((ref = req.status) !== 200 && ref !== 304) {
- a.textContent = "Error " + req.statusText + " (" + req.status + ")";
+ a.textContent = req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error';
return;
}
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler;
@@ -17449,10 +17640,14 @@ Fourchan = (function() {
Fourchan = {
init: function() {
var ref;
- if ((ref = g.VIEW) !== 'index' && ref !== 'thread' && ref !== 'archive') {
+ if (!(Site.software === 'yotsuba' && ((ref = g.VIEW) === 'index' || ref === 'thread' || ref === 'archive'))) {
return;
}
- if (g.BOARD.ID === 'g') {
+ BoardConfig.ready(this.initBoard);
+ return Main.ready(this.initReady);
+ },
+ initBoard: function() {
+ if (g.BOARD.config.code_tags) {
$.on(window, 'prettyprint:cb', function(e) {
var post, pre;
if (!(post = g.posts[e.detail.ID])) {
@@ -17468,11 +17663,15 @@ Fourchan = (function() {
});
$.globalEval('window.addEventListener(\'prettyprint\', function(e) {\n window.dispatchEvent(new CustomEvent(\'prettyprint:cb\', {\n detail: {\n ID: e.detail.ID,\n i: e.detail.i,\n html: prettyPrintOne(e.detail.html)\n }\n }));\n}, false);');
Callbacks.Post.push({
- name: 'Parse /g/ code',
- cb: this.code
+ name: 'Parse [code] tags',
+ cb: Fourchan.code
});
+ g.posts.forEach(function(post) {
+ return Callbacks.Post.execute(post, ['Parse [code] tags'], true);
+ });
+ ExpandComment.callbacks.push(Fourchan.code);
}
- if (g.BOARD.ID === 'sci') {
+ if (g.BOARD.config.math_tags) {
$.global(function() {
return window.addEventListener('mathjax', function(e) {
if (window.MathJax) {
@@ -17491,20 +17690,24 @@ Fourchan = (function() {
}, false);
});
Callbacks.Post.push({
- name: 'Parse /sci/ math',
- cb: this.math
+ name: 'Parse [math] tags',
+ cb: Fourchan.math
});
+ g.posts.forEach(function(post) {
+ return Callbacks.Post.execute(post, ['Parse [math] tags'], true);
+ });
+ return ExpandComment.callbacks.push(Fourchan.math);
}
- return Main.ready(function() {
- return $.global(function() {
- var j, len, node, ref1;
- window.clickable_ids = false;
- ref1 = document.querySelectorAll('.posteruid, .capcode');
- for (j = 0, len = ref1.length; j < len; j++) {
- node = ref1[j];
- node.removeEventListener('click', window.idClick, false);
- }
- });
+ },
+ initReady: function() {
+ return $.global(function() {
+ var j, len, node, ref;
+ window.clickable_ids = false;
+ ref = document.querySelectorAll('.posteruid, .capcode');
+ for (j = 0, len = ref.length; j < len; j++) {
+ node = ref[j];
+ node.removeEventListener('click', window.idClick, false);
+ }
});
},
code: function() {
@@ -17881,6 +18084,12 @@ Keybinds = (function() {
}
ThreadWatcher.toggleWatcher();
break;
+ case Conf['Toggle threading']:
+ if (!QuoteThreading.ready) {
+ return;
+ }
+ QuoteThreading.toggleThreading();
+ break;
case Conf['Mark thread read']:
if (!(g.VIEW === 'index' && thread && UnreadIndex.enabled)) {
return;
@@ -18735,7 +18944,6 @@ Report = (function() {
results = [];
fn = function(name, url) {
return $.ajax(url, {
- responseType: 'json',
onloadend: function() {
results.push([
name, this.response || {
@@ -18745,8 +18953,7 @@ Report = (function() {
if (results.length === urls.length) {
return cb(results);
}
- }
- }, {
+ },
form: form
});
};
@@ -19378,12 +19585,9 @@ ThreadStats = (function() {
return;
}
ThreadStats.timeout = setTimeout(ThreadStats.fetchPage, 2 * $.MINUTE);
- return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadStats.thread.board + "/threads.json", {
- onload: ThreadStats.onThreadsLoad
- }, {
- whenModified: 'ThreadStats',
- bypassCache: true
- });
+ return $.whenModified(Site.urls.threadsListJSON({
+ boardID: ThreadStats.thread.board
+ }), 'ThreadStats', ThreadStats.onThreadsLoad);
},
onThreadsLoad: function() {
var i, j, k, len, len1, len2, page, purgePos, ref, ref1, ref2, thread;
@@ -19592,11 +19796,12 @@ ThreadUpdater = (function() {
}
},
load: function() {
- var req;
- req = ThreadUpdater.req;
- switch (req.status) {
+ if (this !== ThreadUpdater.req) {
+ return;
+ }
+ switch (this.status) {
case 200:
- ThreadUpdater.parse(req);
+ ThreadUpdater.parse(this);
if (ThreadUpdater.thread.isArchived) {
return ThreadUpdater.kill();
} else {
@@ -19604,7 +19809,9 @@ ThreadUpdater = (function() {
}
break;
case 404:
- return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", {
+ return $.ajax(Site.urls.catalogJSON({
+ boardID: ThreadUpdater.thread.board.ID
+ }), {
onloadend: function() {
var confirmed, i, k, len, len1, page, ref, ref1, thread;
if (this.status === 200) {
@@ -19627,12 +19834,12 @@ ThreadUpdater = (function() {
if (confirmed) {
return ThreadUpdater.kill();
} else {
- return ThreadUpdater.error(req);
+ return ThreadUpdater.error(this);
}
}
});
default:
- return ThreadUpdater.error(req);
+ return ThreadUpdater.error(this);
}
}
},
@@ -19710,18 +19917,18 @@ ThreadUpdater = (function() {
return ThreadUpdater.seconds--;
},
update: function() {
- var ref;
+ var oldReq;
clearTimeout(ThreadUpdater.timeoutID);
ThreadUpdater.set('timer', '...', 'loading');
- if ((ref = ThreadUpdater.req) != null) {
- ref.abort();
+ if ((oldReq = ThreadUpdater.req)) {
+ delete ThreadUpdater.req;
+ oldReq.abort();
}
- return ThreadUpdater.req = $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/thread/" + ThreadUpdater.thread + ".json", {
- onloadend: ThreadUpdater.cb.load,
+ return ThreadUpdater.req = $.whenModified(Site.urls.threadJSON({
+ boardID: ThreadUpdater.thread.board.ID,
+ threadID: ThreadUpdater.thread.ID
+ }), 'ThreadUpdater', ThreadUpdater.cb.load, {
timeout: $.MINUTE
- }, {
- whenModified: 'ThreadUpdater',
- bypassCache: true
});
},
updateThreadStatus: function(type, status) {
@@ -19881,6 +20088,7 @@ ThreadWatcher = (function() {
className: 'fa fa-eye'
});
this.db = new DataBoard('watchedThreads', this.refresh, true);
+ this.dbLM = new DataBoard('watcherLastModified', null, true);
this.dialog = UI.dialog('thread-watcher', {
innerHTML: "
"
});
@@ -19911,6 +20119,7 @@ ThreadWatcher = (function() {
this.dialog.hidden = true;
}
Header.addShortcut('watcher', sc, 510);
+ ThreadWatcher.initLastModified();
ThreadWatcher.fetchAuto();
$.on(window, 'visibilitychange focus', function() {
return $.queueTask(ThreadWatcher.fetchAuto);
@@ -19974,7 +20183,7 @@ ThreadWatcher = (function() {
return toggler.title = (isWatched ? 'Unwatch' : 'Watch') + " Thread";
},
node: function() {
- var boardID, data, threadID, toggler;
+ var boardID, data, siteID, threadID, toggler;
if (this.isReply) {
return;
}
@@ -19987,9 +20196,11 @@ ThreadWatcher = (function() {
});
$.before($('input', this.nodes.info), toggler);
}
+ siteID = Site.hostname;
boardID = this.board.ID;
threadID = this.thread.ID;
data = ThreadWatcher.db.get({
+ siteID: siteID,
boardID: boardID,
threadID: threadID
});
@@ -19998,14 +20209,11 @@ ThreadWatcher = (function() {
if (data && (data.excerpt == null)) {
return $.queueTask((function(_this) {
return function() {
- ThreadWatcher.db.extend({
- boardID: boardID,
- threadID: threadID,
+ return ThreadWatcher.update(siteID, boardID, threadID, {
val: {
excerpt: Get.threadExcerpt(_this.thread)
}
});
- return ThreadWatcher.refresh();
};
})(this));
}
@@ -20039,25 +20247,25 @@ ThreadWatcher = (function() {
},
cb: {
openAll: function() {
- var a, i, len1, ref;
+ var a, j, len1, ref;
if ($.hasClass(this, 'disabled')) {
return;
}
ref = $$('a[title]', ThreadWatcher.list);
- for (i = 0, len1 = ref.length; i < len1; i++) {
- a = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ a = ref[j];
$.open(a.href);
}
return $.event('CloseMenu');
},
pruneDeads: function() {
- var boardID, data, i, len1, ref, ref1, siteID, threadID;
+ var boardID, data, j, len1, ref, ref1, siteID, threadID;
if ($.hasClass(this, 'disabled')) {
return;
}
ref = ThreadWatcher.getAll();
- for (i = 0, len1 = ref.length; i < len1; i++) {
- ref1 = ref[i], siteID = ref1.siteID, boardID = ref1.boardID, threadID = ref1.threadID, data = ref1.data;
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ ref1 = ref[j], siteID = ref1.siteID, boardID = ref1.boardID, threadID = ref1.threadID, data = ref1.data;
if (data.isDead) {
ThreadWatcher.db["delete"]({
siteID: siteID,
@@ -20108,28 +20316,32 @@ ThreadWatcher = (function() {
})) {
continue;
}
- nKilled++;
if (Conf['Auto Prune'] || !(data && typeof data === 'object')) {
db["delete"]({
boardID: boardID,
threadID: threadID
});
+ nKilled++;
+ } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ ThreadWatcher.fetchStatus({
+ siteID: siteID,
+ boardID: boardID,
+ threadID: threadID,
+ data: data
+ });
} else {
db.extend({
boardID: boardID,
threadID: threadID,
val: {
- isDead: true
+ isDead: true,
+ page: void 0,
+ lastPage: void 0,
+ unread: void 0,
+ quotingYou: void 0
}
});
- if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
- ThreadWatcher.fetchStatus({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- data: data
- });
- }
+ nKilled++;
}
}
if (nKilled) {
@@ -20147,6 +20359,38 @@ ThreadWatcher = (function() {
},
requests: [],
fetched: 0,
+ fetch: function(url, arg, args, cb) {
+ var ajax, force, onloadend, ref, req, siteID;
+ siteID = arg.siteID, force = arg.force;
+ if (ThreadWatcher.requests.length === 0) {
+ ThreadWatcher.status.textContent = '...';
+ $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
+ }
+ onloadend = function() {
+ if (this.finished) {
+ return;
+ }
+ this.finished = true;
+ ThreadWatcher.fetched++;
+ if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
+ ThreadWatcher.clearRequests();
+ } else {
+ ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%";
+ }
+ return cb.apply(this, args);
+ };
+ ajax = siteID === Site.hostname ? $.ajax : CrossOrigin.ajax;
+ if (force) {
+ if ((ref = $.lastModified.ThreadWatcher) != null) {
+ delete ref[url];
+ }
+ }
+ req = $.whenModified(url, 'ThreadWatcher', onloadend, {
+ timeout: $.MINUTE,
+ ajax: ajax
+ });
+ return ThreadWatcher.requests.push(req);
+ },
clearRequests: function() {
ThreadWatcher.requests = [];
ThreadWatcher.fetched = 0;
@@ -20154,16 +20398,45 @@ ThreadWatcher = (function() {
return $.rmClass(ThreadWatcher.refreshButton, 'fa-spin');
},
abort: function() {
- var i, len1, ref, req;
+ var j, len1, ref, req;
+ delete ThreadWatcher.syncing;
ref = ThreadWatcher.requests;
- for (i = 0, len1 = ref.length; i < len1; i++) {
- req = ref[i];
- if (req.readyState !== 4) {
- req.abort();
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ req = ref[j];
+ if (!(!req.finished)) {
+ continue;
}
+ req.finished = true;
+ req.abort();
}
return ThreadWatcher.clearRequests();
},
+ initLastModified: function() {
+ var base, boardID, boards, data, date, lm, ref, ref1, siteID, url;
+ lm = ((base = $.lastModified)['ThreadWatcher'] || (base['ThreadWatcher'] = {}));
+ ref = ThreadWatcher.dbLM.data;
+ for (siteID in ref) {
+ boards = ref[siteID];
+ ref1 = boards.boards;
+ for (boardID in ref1) {
+ data = ref1[boardID];
+ if (ThreadWatcher.db.get({
+ siteID: siteID,
+ boardID: boardID
+ })) {
+ for (url in data) {
+ date = data[url];
+ lm[url] = date;
+ }
+ } else {
+ ThreadWatcher.dbLM["delete"]({
+ siteID: siteID,
+ boardID: boardID
+ });
+ }
+ }
+ }
+ },
fetchAuto: function() {
var db, interval, now, ref;
clearTimeout(ThreadWatcher.timeout);
@@ -20171,46 +20444,169 @@ ThreadWatcher = (function() {
return;
}
db = ThreadWatcher.db;
- interval = ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] ? 5 * $.MINUTE : 2 * $.HOUR;
+ interval = Conf['Show Page'] || (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) ? 5 * $.MINUTE : 2 * $.HOUR;
now = Date.now();
if (!((now - interval < (ref = db.data.lastChecked || 0) && ref <= now) || d.hidden || !d.hasFocus())) {
ThreadWatcher.fetchAllStatus();
- db.setLastChecked();
}
return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval);
},
buttonFetchAll: function() {
- if (ThreadWatcher.requests.length) {
+ if (ThreadWatcher.syncing || ThreadWatcher.requests.length) {
return ThreadWatcher.abort();
} else {
return ThreadWatcher.fetchAllStatus();
}
},
fetchAllStatus: function() {
- var db, dbs, i, len1, n, results;
+ var dbi, dbs, j, len1, n, results;
+ ThreadWatcher.status.textContent = '...';
+ $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
+ ThreadWatcher.syncing = true;
dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(function(x) {
return x;
});
n = 0;
results = [];
- for (i = 0, len1 = dbs.length; i < len1; i++) {
- db = dbs[i];
- results.push(db.forceSync(function() {
- var j, len2, thread, threads;
+ for (j = 0, len1 = dbs.length; j < len1; j++) {
+ dbi = dbs[j];
+ results.push(dbi.forceSync(function() {
+ var board, boards, db, deep, k, len2, now, ref;
if ((++n) === dbs.length) {
- threads = ThreadWatcher.getAll();
- for (j = 0, len2 = threads.length; j < len2; j++) {
- thread = threads[j];
- ThreadWatcher.fetchStatus(thread);
+ if (!ThreadWatcher.syncing) {
+ return;
+ }
+ delete ThreadWatcher.syncing;
+ db = ThreadWatcher.db;
+ now = Date.now();
+ deep = !((now - 2 * $.HOUR < (ref = db.data.lastChecked2 || 0) && ref <= now));
+ boards = ThreadWatcher.getAll(true);
+ for (k = 0, len2 = boards.length; k < len2; k++) {
+ board = boards[k];
+ ThreadWatcher.fetchBoard(board, deep);
+ }
+ db.setLastChecked();
+ if (deep) {
+ db.setLastChecked('lastChecked2');
+ }
+ if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
+ return ThreadWatcher.clearRequests();
}
}
}));
}
return results;
},
- fetchStatus: function(thread, force) {
- var base, boardID, data, ref, ref1, req, siteID, software, threadID, url;
- siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data;
+ fetchBoard: function(board, deep) {
+ var base, boardID, force, ref, ref1, ref2, siteID, software, url, urlF;
+ if (!board.some(function(thread) {
+ return !thread.data.isDead;
+ })) {
+ return;
+ }
+ force = Conf['Show Page'] && board.some(function(thread) {
+ return (thread.data.page == null) && !thread.data.isDead && thread.data.last !== -1;
+ });
+ ref = board[0], siteID = ref.siteID, boardID = ref.boardID;
+ software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0;
+ urlF = deep && software === 'tinyboard' ? 'catalogJSON' : 'threadsListJSON';
+ url = (ref2 = SW[software]) != null ? typeof (base = ref2.urls)[urlF] === "function" ? base[urlF]({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0 : void 0;
+ if (!url) {
+ return;
+ }
+ return ThreadWatcher.fetch(url, {
+ siteID: siteID,
+ force: force
+ }, [board, url], ThreadWatcher.parseBoard);
+ },
+ parseBoard: function(board, url) {
+ var boardID, data, i, index, item, j, k, l, lastPage, len1, len2, len3, len4, lmDate, m, modified, nThreads, oldest, page, pageLength, ref, ref1, ref2, ref3, ref4, ref5, ref6, replies, siteID, software, thread, threadID, threads;
+ if (this.status !== 200) {
+ return;
+ }
+ ref = board[0], siteID = ref.siteID, boardID = ref.boardID;
+ software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0;
+ lmDate = this.getResponseHeader('Last-Modified');
+ ThreadWatcher.dbLM.extend({
+ siteID: siteID,
+ boardID: boardID,
+ val: $.item(url, lmDate)
+ });
+ threads = {};
+ pageLength = 0;
+ nThreads = 0;
+ oldest = null;
+ try {
+ pageLength = ((ref2 = this.response[0]) != null ? ref2.threads.length : void 0) || 0;
+ ref3 = this.response;
+ for (i = j = 0, len1 = ref3.length; j < len1; i = ++j) {
+ page = ref3[i];
+ ref4 = page.threads;
+ for (k = 0, len2 = ref4.length; k < len2; k++) {
+ item = ref4[k];
+ threads[item.no] = {
+ page: i + 1,
+ index: nThreads,
+ modified: item.last_modified,
+ replies: item.replies
+ };
+ nThreads++;
+ if ((oldest == null) || item.no < oldest) {
+ oldest = item.no;
+ }
+ }
+ }
+ } catch (_error) {
+ for (l = 0, len3 = board.length; l < len3; l++) {
+ thread = board[l];
+ ThreadWatcher.fetchStatus(thread);
+ }
+ }
+ for (m = 0, len4 = board.length; m < len4; m++) {
+ thread = board[m];
+ threadID = thread.threadID, data = thread.data;
+ if (threads[threadID]) {
+ ref5 = threads[threadID], page = ref5.page, index = ref5.index, modified = ref5.modified, replies = ref5.replies;
+ if (Conf['Show Page']) {
+ lastPage = ((ref6 = SW[software]) != null ? typeof ref6.isPrunedByAge === "function" ? ref6.isPrunedByAge({
+ siteID: siteID,
+ boardID: boardID
+ }) : void 0 : void 0) ? threadID === oldest : index >= nThreads - pageLength;
+ ThreadWatcher.update(siteID, boardID, threadID, {
+ page: page,
+ lastPage: lastPage
+ });
+ }
+ if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ if (modified !== data.modified || ((replies != null) && replies !== data.replies)) {
+ ThreadWatcher.db.extend({
+ siteID: siteID,
+ boardID: boardID,
+ threadID: threadID,
+ val: {
+ modified: modified
+ }
+ });
+ ThreadWatcher.fetchStatus(thread);
+ }
+ }
+ } else {
+ if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ ThreadWatcher.fetchStatus(thread);
+ } else {
+ ThreadWatcher.update(siteID, boardID, threadID, {
+ isDead: true
+ });
+ }
+ }
+ }
+ },
+ fetchStatus: function(thread) {
+ var base, boardID, data, force, ref, ref1, siteID, software, threadID, url;
+ siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data, force = thread.force;
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
url = (ref1 = SW[software]) != null ? typeof (base = ref1.urls).threadJSON === "function" ? base.threadJSON({
siteID: siteID,
@@ -20226,54 +20622,21 @@ ThreadWatcher = (function() {
if (data.last === -1) {
return;
}
- if (ThreadWatcher.requests.length === 0) {
- ThreadWatcher.status.textContent = '...';
- $.addClass(ThreadWatcher.refreshButton, 'fa-spin');
- }
- if ((typeof Site.hasCORS === "function" ? Site.hasCORS(url) : void 0) || url.split('/').slice(0, 3).join('/') === location.origin) {
- req = $.ajax(url, {
- onloadend: function() {
- return ThreadWatcher.parseStatus.call(this, thread);
- },
- timeout: $.MINUTE
- }, {
- whenModified: force ? false : 'ThreadWatcher'
- });
- } else {
- req = {
- abort: function() {
- return req.aborted = true;
- }
- };
- CrossOrigin.json(url, function() {
- if (req.aborted) {
- return;
- }
- return ThreadWatcher.parseStatus.call(this, thread);
- }, true, $.MINUTE);
- }
- return ThreadWatcher.requests.push(req);
+ return ThreadWatcher.fetch(url, {
+ siteID: siteID,
+ force: force
+ }, [thread], ThreadWatcher.parseStatus);
},
parseStatus: function(arg) {
- var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, siteID, software, threadID, unread, updated, youOP;
+ var boardID, data, isDead, j, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, replies, siteID, software, threadID, unread, youOP;
siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, data = arg.data;
- ThreadWatcher.fetched++;
- if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {
- ThreadWatcher.clearRequests();
- } else {
- ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%";
- }
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
if (this.status === 200 && this.response) {
last = this.response.posts[this.response.posts.length - 1].no;
+ replies = this.response.posts.length - 1;
isDead = !!this.response.posts[0].archived;
if (isDead && Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID
- });
- ThreadWatcher.refresh();
+ ThreadWatcher.rm(siteID, boardID, threadID);
return;
}
if (last === data.last && isDead === data.isDead) {
@@ -20294,8 +20657,8 @@ ThreadWatcher = (function() {
postID: threadID
}) : void 0);
ref2 = this.response.posts;
- for (i = 0, len1 = ref2.length; i < len1; i++) {
- postObj = ref2[i];
+ for (j = 0, len1 = ref2.length; j < len1; j++) {
+ postObj = ref2[j];
if (!(postObj.no > lastReadPost)) {
continue;
}
@@ -20308,7 +20671,7 @@ ThreadWatcher = (function() {
continue;
}
unread++;
- if (!quotingYou && !Conf['Require OP Quote Link'] && youOP && !Filter.isHidden(Build.parseJSON(postObj, boardID))) {
+ if (!quotingYou && !Conf['Require OP Quote Link'] && youOP && !Filter.isHidden(Build.parseJSON(postObj, boardID, siteID))) {
quotingYou = true;
continue;
}
@@ -20329,58 +20692,31 @@ ThreadWatcher = (function() {
break;
}
}
- if (quotesYou && !Filter.isHidden(Build.parseJSON(postObj, boardID))) {
+ if (quotesYou && !Filter.isHidden(Build.parseJSON(postObj, boardID, siteID))) {
quotingYou = true;
}
}
- updated = isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou;
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- last: last,
- isDead: isDead,
- unread: unread,
- quotingYou: quotingYou
- }
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ last: last,
+ replies: replies,
+ isDead: isDead,
+ unread: unread,
+ quotingYou: quotingYou
});
- if (updated) {
- return ThreadWatcher.refresh();
- }
} else if (this.status === 404) {
if (SW[software].mayLackJSON && (data.last == null)) {
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- last: -1
- },
- rm: ['unread', 'quotingYou']
- });
- } else if (Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ last: -1
});
} else {
- ThreadWatcher.db.extend({
- siteID: siteID,
- boardID: boardID,
- threadID: threadID,
- val: {
- isDead: true
- },
- rm: ['unread', 'quotingYou']
+ return ThreadWatcher.update(siteID, boardID, threadID, {
+ isDead: true
});
}
- return ThreadWatcher.refresh();
}
},
- getAll: function() {
- var all, boardID, boards, data, ref, ref1, siteID, threadID, threads;
+ getAll: function(groupByBoard) {
+ var all, boardID, boards, cont, data, ref, ref1, siteID, threadID, threads;
all = [];
ref = ThreadWatcher.db.data;
for (siteID in ref) {
@@ -20391,10 +20727,13 @@ ThreadWatcher = (function() {
if (Conf['Current Board'] && (siteID !== Site.hostname || boardID !== g.BOARD.ID)) {
continue;
}
+ if (groupByBoard) {
+ all.push((cont = []));
+ }
for (threadID in threads) {
data = threads[threadID];
if (data && typeof data === 'object') {
- all.push({
+ (groupByBoard ? cont : all).push({
siteID: siteID,
boardID: boardID,
threadID: threadID,
@@ -20407,7 +20746,7 @@ ThreadWatcher = (function() {
return all;
},
makeLine: function(siteID, boardID, threadID, data) {
- var count, div, excerpt, fullID, link, ref, ref1, software, title, x;
+ var count, div, excerpt, fullID, link, page, ref, ref1, software, title, x;
software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0;
x = $.el('a', {
className: 'fa fa-times',
@@ -20428,6 +20767,13 @@ ThreadWatcher = (function() {
title: excerpt,
className: 'watcher-link'
});
+ if (Conf['Show Page'] && (data.page != null)) {
+ page = $.el('span', {
+ textContent: "[" + data.page + "]",
+ className: 'watcher-page'
+ });
+ $.add(link, page);
+ }
if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] && (data.unread != null)) {
count = $.el('span', {
textContent: "(" + data.unread + ")",
@@ -20450,6 +20796,14 @@ ThreadWatcher = (function() {
if (data.isDead) {
$.addClass(div, 'dead-thread');
}
+ if (Conf['Show Page']) {
+ if (data.lastPage) {
+ $.addClass(div, 'last-page');
+ }
+ if (data.page != null) {
+ div.dataset.page = data.page;
+ }
+ }
if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
if (data.unread === 0) {
$.addClass(div, 'replies-read');
@@ -20465,10 +20819,10 @@ ThreadWatcher = (function() {
return div;
},
setPrefixes: function(threads) {
- var conflicts, conflicts2, i, j, len, len1, len2, prefix, prefixes, siteID, siteID2;
+ var conflicts, conflicts2, j, k, len, len1, len2, prefix, prefixes, siteID, siteID2;
prefixes = {};
- for (i = 0, len1 = threads.length; i < len1; i++) {
- siteID = threads[i].siteID;
+ for (j = 0, len1 = threads.length; j < len1; j++) {
+ siteID = threads[j].siteID;
if (siteID in prefixes) {
continue;
}
@@ -20479,8 +20833,8 @@ ThreadWatcher = (function() {
len++;
prefix = siteID.slice(0, len);
conflicts2 = [];
- for (j = 0, len2 = conflicts.length; j < len2; j++) {
- siteID2 = conflicts[j];
+ for (k = 0, len2 = conflicts.length; k < len2; k++) {
+ siteID2 = conflicts[k];
if (siteID2.slice(0, len) === prefix) {
conflicts2.push(siteID2);
} else if (prefixes[siteID2].length < len) {
@@ -20494,12 +20848,12 @@ ThreadWatcher = (function() {
return ThreadWatcher.prefixes = prefixes;
},
build: function() {
- var boardID, data, i, j, len1, len2, list, nodes, ref, ref1, refresher, siteID, thread, threadID, threads;
+ var boardID, data, j, len1, list, nodes, ref, siteID, thread, threadID, threads;
nodes = [];
threads = ThreadWatcher.getAll();
ThreadWatcher.setPrefixes(threads);
- for (i = 0, len1 = threads.length; i < len1; i++) {
- ref = threads[i], siteID = ref.siteID, boardID = ref.boardID, threadID = ref.threadID, data = ref.data;
+ for (j = 0, len1 = threads.length; j < len1; j++) {
+ ref = threads[j], siteID = ref.siteID, boardID = ref.boardID, threadID = ref.threadID, data = ref.data;
if ((data.excerpt == null) && siteID === Site.hostname && (thread = g.threads[boardID + "." + threadID]) && thread.OP) {
ThreadWatcher.db.extend({
boardID: boardID,
@@ -20514,22 +20868,17 @@ ThreadWatcher = (function() {
list = ThreadWatcher.list;
$.rmAll(list);
$.add(list, nodes);
- ThreadWatcher.refreshIcon();
- ref1 = ThreadWatcher.menu.refreshers;
- for (j = 0, len2 = ref1.length; j < len2; j++) {
- refresher = ref1[j];
- refresher();
- }
+ return ThreadWatcher.refreshIcon();
},
refresh: function() {
ThreadWatcher.build();
g.threads.forEach(function(thread) {
- var i, isWatched, len1, post, ref, toggler;
+ var isWatched, j, len1, post, ref, toggler;
isWatched = ThreadWatcher.isWatched(thread);
if (thread.OP) {
ref = [thread.OP].concat(slice.call(thread.OP.clones));
- for (i = 0, len1 = ref.length; i < len1; i++) {
- post = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ post = ref[j];
if ((toggler = $('.watch-thread-link', post.nodes.info))) {
ThreadWatcher.setToggler(toggler, isWatched);
}
@@ -20546,30 +20895,35 @@ ThreadWatcher = (function() {
}
},
refreshIcon: function() {
- var className, i, len1, ref;
+ var className, j, len1, ref;
ref = ['replies-unread', 'replies-quoting-you'];
- for (i = 0, len1 = ref.length; i < len1; i++) {
- className = ref[i];
+ for (j = 0, len1 = ref.length; j < len1; j++) {
+ className = ref[j];
ThreadWatcher.shortcut.classList.toggle(className, !!$("." + className, ThreadWatcher.dialog));
}
},
- update: function(boardID, threadID, newData) {
- var data, key, line, n, newLine, ref, siteID, val;
- siteID = Site.hostname;
+ update: function(siteID, boardID, threadID, newData) {
+ var data, j, key, len1, line, n, newLine, ref, ref1, val;
if (!(data = (ref = ThreadWatcher.db) != null ? ref.get({
+ siteID: siteID,
boardID: boardID,
threadID: threadID
}) : void 0)) {
return;
}
if (newData.isDead && Conf['Auto Prune']) {
- ThreadWatcher.db["delete"]({
- boardID: boardID,
- threadID: threadID
- });
- ThreadWatcher.refresh();
+ ThreadWatcher.rm(siteID, boardID, threadID);
return;
}
+ if (newData.isDead || newData.last === -1) {
+ ref1 = ['page', 'lastPage', 'unread', 'quotingyou'];
+ for (j = 0, len1 = ref1.length; j < len1; j++) {
+ key = ref1[j];
+ if (!(key in newData)) {
+ newData[key] = void 0;
+ }
+ }
+ }
n = 0;
for (key in newData) {
val = newData[key];
@@ -20580,18 +20934,13 @@ ThreadWatcher = (function() {
if (!n) {
return;
}
- if (!(data = ThreadWatcher.db.get({
- boardID: boardID,
- threadID: threadID
- }))) {
- return;
- }
ThreadWatcher.db.extend({
+ siteID: siteID,
boardID: boardID,
threadID: threadID,
val: newData
});
- if (line = $("#watched-threads > [data-site-i-d='" + siteID + "'][data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog)) {
+ if ((line = $("#watched-threads > [data-site-i-d='" + siteID + "'][data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog))) {
newLine = ThreadWatcher.makeLine(siteID, boardID, threadID, data);
$.replace(line, newLine);
return ThreadWatcher.refreshIcon();
@@ -20614,16 +20963,19 @@ ThreadWatcher = (function() {
});
return cb();
}
- if (data.isDead && !((data.unread != null) || (data.quotingYou != null))) {
+ if (data.isDead && !((data.page != null) || (data.lastPage != null) || (data.unread != null) || (data.quotingYou != null))) {
return cb();
}
return ThreadWatcher.db.extend({
boardID: boardID,
threadID: threadID,
val: {
- isDead: true
- },
- rm: ['unread', 'quotingYou']
+ isDead: true,
+ page: void 0,
+ lastPage: void 0,
+ unread: void 0,
+ quotingYou: void 0
+ }
}, cb);
},
toggle: function(thread) {
@@ -20662,19 +21014,24 @@ ThreadWatcher = (function() {
return ThreadWatcher.addRaw(boardID, threadID, data);
},
addRaw: function(boardID, threadID, data) {
+ var thread;
ThreadWatcher.db.set({
boardID: boardID,
threadID: threadID,
val: data
});
ThreadWatcher.refresh();
- if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
- return ThreadWatcher.fetchStatus({
- siteID: Site.hostname,
- boardID: boardID,
- threadID: threadID,
- data: data
- }, true);
+ thread = {
+ siteID: Site.hostname,
+ boardID: boardID,
+ threadID: threadID,
+ data: data,
+ force: true
+ };
+ if (Conf['Show Page'] && !data.isDead) {
+ return ThreadWatcher.fetchBoard([thread]);
+ } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {
+ return ThreadWatcher.fetchStatus(thread);
}
},
rm: function(siteID, boardID, threadID) {
@@ -20686,7 +21043,6 @@ ThreadWatcher = (function() {
return ThreadWatcher.refresh();
},
menu: {
- refreshers: [],
init: function() {
var menu;
if (!Conf['Thread Watcher']) {
@@ -20708,73 +21064,61 @@ ThreadWatcher = (function() {
});
Header.menu.addEntry({
el: entryEl,
- order: 60
+ order: 60,
+ open: function() {
+ var addClass, ref, rmClass, text;
+ ref = !!ThreadWatcher.db.get({
+ boardID: g.BOARD.ID,
+ threadID: g.THREADID
+ }) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = ref[0], rmClass = ref[1], text = ref[2];
+ $.addClass(entryEl, addClass);
+ $.rmClass(entryEl, rmClass);
+ entryEl.textContent = text;
+ return true;
+ }
});
- $.on(entryEl, 'click', function() {
+ return $.on(entryEl, 'click', function() {
return ThreadWatcher.toggle(g.threads[g.BOARD + "." + g.THREADID]);
});
- return this.refreshers.push(function() {
- var addClass, ref, rmClass, text;
- ref = $('.current', ThreadWatcher.list) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = ref[0], rmClass = ref[1], text = ref[2];
- $.addClass(entryEl, addClass);
- $.rmClass(entryEl, rmClass);
- return entryEl.textContent = text;
- });
},
addMenuEntries: function() {
- var cb, conf, entries, entry, i, len1, name, ref, ref1, refresh, subEntries;
+ var cb, conf, entries, entry, j, len1, name, open, ref, ref1, text;
entries = [];
entries.push({
+ text: 'Open all threads',
cb: ThreadWatcher.cb.openAll,
- entry: {
- el: $.el('a', {
- textContent: 'Open all threads'
- })
- },
- refresh: function() {
- return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled');
+ open: function() {
+ this.el.classList.toggle('disabled', !ThreadWatcher.list.firstElementChild);
+ return true;
}
});
entries.push({
+ text: 'Prune dead threads',
cb: ThreadWatcher.cb.pruneDeads,
- entry: {
+ open: function() {
+ this.el.classList.toggle('disabled', !$('.dead-thread', ThreadWatcher.list));
+ return true;
+ }
+ });
+ for (j = 0, len1 = entries.length; j < len1; j++) {
+ ref = entries[j], text = ref.text, cb = ref.cb, open = ref.open;
+ entry = {
el: $.el('a', {
- textContent: 'Prune dead threads'
+ textContent: text,
+ href: 'javascript:;'
})
- },
- refresh: function() {
- return ($('.dead-thread', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled');
- }
- });
- subEntries = [];
- ref = Config.threadWatcher;
- for (name in ref) {
- conf = ref[name];
- subEntries.push(this.createSubEntry(name, conf[1]));
- }
- entries.push({
- entry: {
- el: $.el('span', {
- textContent: 'Settings'
- }),
- subEntries: subEntries
- }
- });
- for (i = 0, len1 = entries.length; i < len1; i++) {
- ref1 = entries[i], entry = ref1.entry, cb = ref1.cb, refresh = ref1.refresh;
- if (entry.el.nodeName === 'A') {
- entry.el.href = 'javascript:;';
- }
- if (cb) {
- $.on(entry.el, 'click', cb);
- }
- if (refresh) {
- this.refreshers.push(refresh.bind(entry));
- }
+ };
+ $.on(entry.el, 'click', cb);
+ entry.open = open.bind(entry);
this.menu.addEntry(entry);
}
+ ref1 = Config.threadWatcher;
+ for (name in ref1) {
+ conf = ref1[name];
+ this.addCheckbox(name, conf[1]);
+ }
},
- createSubEntry: function(name, desc) {
+ addCheckbox: function(name, desc) {
var entry, input;
entry = {
type: 'thread watcher',
@@ -20788,13 +21132,13 @@ ThreadWatcher = (function() {
entry.el.title += '\n[Remember Last Read Post is disabled.]';
}
$.on(input, 'change', $.cb.checked);
- if (name === 'Current Board' || name === 'Show Unread Count' || name === 'Show Site Prefix') {
+ if (name === 'Current Board' || name === 'Show Page' || name === 'Show Unread Count' || name === 'Show Site Prefix') {
$.on(input, 'change', ThreadWatcher.refresh);
}
- if (name === 'Show Unread Count' || name === 'Auto Update Thread Watcher') {
+ if (name === 'Show Page' || name === 'Show Unread Count' || name === 'Auto Update Thread Watcher') {
$.on(input, 'change', ThreadWatcher.fetchAuto);
}
- return entry;
+ return this.menu.addEntry(entry);
}
}
};
@@ -20948,12 +21292,15 @@ Unread = (function() {
return;
}
},
- openNotification: function(post) {
+ openNotification: function(post, predicate) {
var notif;
+ if (predicate == null) {
+ predicate = ' replied to you';
+ }
if (!Header.areNotificationsEnabled) {
return;
}
- notif = new Notification(post.info.nameBlock + " replied to you", {
+ notif = new Notification("" + post.info.nameBlock + predicate, {
body: post.commentDisplay(),
icon: Favicon.logo
});
@@ -21083,7 +21430,7 @@ Unread = (function() {
saveThreadWatcherCount: $.debounce(2 * $.SECOND, function() {
$.forceSync('Remember Last Read Post');
if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) {
- return ThreadWatcher.update(Unread.thread.board.ID, Unread.thread.ID, {
+ return ThreadWatcher.update(Site.hostname, Unread.thread.board.ID, Unread.thread.ID, {
isDead: Unread.thread.isDead,
unread: Unread.posts.size,
quotingYou: !!(!Conf['Require OP Quote Link'] && QuoteYou.isYou(Unread.thread.OP) ? Unread.posts.size : Unread.postsQuotingYou.size)
@@ -21216,16 +21563,12 @@ UnreadIndex = (function() {
markRead: function() {
var lastPost, thread;
thread = Get.threadFromNode(this);
- if (Index.enabled) {
- lastPost = Index.lastPost(thread.ID);
- } else {
- lastPost = 0;
- thread.posts.forEach(function(post) {
- if (post.ID > lastPost && !post.isFetchedQuote) {
- return lastPost = post.ID;
- }
- });
- }
+ lastPost = Index.enabled ? Index.lastPost(thread.ID) : 0;
+ thread.posts.forEach(function(post) {
+ if (post.ID > lastPost && !post.isFetchedQuote) {
+ return lastPost = post.ID;
+ }
+ });
UnreadIndex.lastReadPost[thread.fullID] = lastPost;
UnreadIndex.db.set({
boardID: thread.board.ID,
@@ -21234,7 +21577,7 @@ UnreadIndex = (function() {
});
$.rm(UnreadIndex.hr[thread.fullID]);
thread.nodes.root.classList.remove('unread-thread');
- return ThreadWatcher.update(thread.board.ID, thread.ID, {
+ return ThreadWatcher.update(Site.hostname, thread.board.ID, thread.ID, {
unread: 0,
quotingYou: false
});
@@ -22729,7 +23072,7 @@ QR = (function() {
}
},
submit: function(e) {
- var captcha, cb, err, extra, filetag, formData, options, post, ref, thread, threadID;
+ var captcha, cb, err, filetag, formData, options, post, ref, thread, threadID;
if (e != null) {
e.preventDefault();
}
@@ -22799,47 +23142,35 @@ QR = (function() {
options = {
responseType: 'document',
withCredentials: true,
- onload: QR.response,
- onerror: function() {
- delete QR.req;
- if (QR.currentCaptcha) {
- Captcha.cache.save(QR.currentCaptcha);
- }
- delete QR.currentCaptcha;
- post.unlock();
- QR.cooldown.auto = true;
- QR.cooldown.addDelay(post, 2);
- QR.status();
- return QR.error(QR.connectionError());
- }
- };
- extra = {
+ onloadend: QR.response,
form: $.formData(formData)
};
if (Conf['Show Upload Progress']) {
- extra.upCallbacks = {
- onload: function() {
+ options.onprogress = function(e) {
+ var ref1;
+ if (this !== ((ref1 = QR.req) != null ? ref1.upload : void 0)) {
+ return;
+ }
+ if (e.loaded < e.total) {
+ QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%";
+ } else {
QR.req.isUploadFinished = true;
QR.req.progress = '...';
- return QR.status();
- },
- onprogress: function(e) {
- QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%";
- return QR.status();
}
+ return QR.status();
};
}
cb = function(response) {
if (response != null) {
QR.currentCaptcha = response;
if (response.challenge != null) {
- extra.form.append('recaptcha_challenge_field', response.challenge);
- extra.form.append('recaptcha_response_field', response.response);
+ options.form.append('recaptcha_challenge_field', response.challenge);
+ options.form.append('recaptcha_response_field', response.response);
} else {
- extra.form.append('g-recaptcha-response', response.response);
+ options.form.append('g-recaptcha-response', response.response);
}
}
- QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options, extra);
+ QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options);
return QR.req.progress = '...';
};
if (typeof captcha === 'function') {
@@ -22865,23 +23196,24 @@ QR = (function() {
return QR.status();
},
response: function() {
- var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, req, resDoc, seconds, threadID;
- req = QR.req;
+ var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, ref3, seconds, threadID;
+ if (this !== QR.req) {
+ return;
+ }
delete QR.req;
post = QR.posts[0];
post.unlock();
- resDoc = req.response;
- if ((err = resDoc.getElementById('errmsg'))) {
- if ((ref = $('a', err)) != null) {
- ref.target = '_blank';
+ if ((err = (ref = this.response) != null ? ref.getElementById('errmsg') : void 0)) {
+ if ((ref1 = $('a', err)) != null) {
+ ref1.target = '_blank';
}
- } else if ((connErr = resDoc.title !== 'Post successful!')) {
+ } else if ((connErr = !this.response || this.response.title !== 'Post successful!')) {
err = QR.connectionError();
if (QR.currentCaptcha) {
Captcha.cache.save(QR.currentCaptcha);
}
- } else if (req.status !== 200) {
- err = "Error " + req.statusText + " (" + req.status + ")";
+ } else if (this.status !== 200) {
+ err = "Error " + this.statusText + " (" + this.status + ")";
}
delete QR.currentCaptcha;
if (err) {
@@ -22904,13 +23236,13 @@ QR = (function() {
} else {
QR.cooldown.auto = false;
}
- QR.captcha.setup(QR.cooldown.auto && ((ref1 = d.activeElement) === QR.nodes.status || ref1 === d.body));
+ QR.captcha.setup(QR.cooldown.auto && ((ref2 = d.activeElement) === QR.nodes.status || ref2 === d.body));
QR.status();
QR.error(err);
return;
}
- h1 = $('h1', resDoc);
- ref2 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref2[0], threadID = ref2[1], postID = ref2[2];
+ h1 = $('h1', this.response);
+ ref3 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref3[0], threadID = ref3[1], postID = ref3[2];
postID = +postID;
threadID = +threadID || postID;
isReply = threadID !== postID;
@@ -22927,10 +23259,10 @@ QR = (function() {
postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount && isReply;
lastPostToThread = !((function() {
- var j, len, p, ref3;
- ref3 = QR.posts.slice(1);
- for (j = 0, len = ref3.length; j < len; j++) {
- p = ref3[j];
+ var j, len, p, ref4;
+ ref4 = QR.posts.slice(1);
+ for (j = 0, len = ref4.length; j < len; j++) {
+ p = ref4[j];
if (p.thread === post.thread) {
return true;
}
@@ -22981,17 +23313,18 @@ QR = (function() {
} else {
return setTimeout(check, attempts * $.SECOND);
}
- }
- }, {
+ },
+ responseType: 'text',
type: 'HEAD'
});
};
return check();
},
abort: function() {
- if (QR.req && !QR.req.isUploadFinished) {
- QR.req.abort();
+ var oldReq;
+ if ((oldReq = QR.req) && !QR.req.isUploadFinished) {
delete QR.req;
+ oldReq.abort();
if (QR.currentCaptcha) {
Captcha.cache.save(QR.currentCaptcha);
}
@@ -24669,6 +25002,14 @@ QuoteThreading =
parent: {},
children: {},
inserted: {},
+ toggleThreading: function() {
+ return this.setThreadingState(!Conf['Thread Quotes']);
+ },
+ setThreadingState: function(enabled) {
+ this.input.checked = enabled;
+ this.setEnabled.call(this.input);
+ return this.rethread.call(this.input);
+ },
setEnabled: function() {
var other, ref;
if (this.checked) {
@@ -25184,9 +25525,6 @@ Main = (function() {
Main = {
init: function() {
var db, flatten, i, items, j, k, key, len, ref, ref1, ref2, w;
- if (d.body && !$('title', d.head)) {
- return;
- }
try {
w = window;
if ($.platform === 'crx') {
diff --git a/builds/4chan-X.zip b/builds/4chan-X.zip
index 6854de245..eb1c9adba 100644
Binary files a/builds/4chan-X.zip and b/builds/4chan-X.zip differ
diff --git a/builds/updates-beta.json b/builds/updates-beta.json
index 8a22664d0..865f12f76 100644
--- a/builds/updates-beta.json
+++ b/builds/updates-beta.json
@@ -3,7 +3,7 @@
"4chan-x@4chan-x.net": {
"updates": [
{
- "version": "1.14.5.13",
+ "version": "1.14.7.2",
"update_link": "https://www.4chan-x.net/builds/4chan-X-beta.crx"
}
]
diff --git a/builds/updates-beta.xml b/builds/updates-beta.xml
index cd87d0c64..03fd2bfb8 100644
--- a/builds/updates-beta.xml
+++ b/builds/updates-beta.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/builds/updates.json b/builds/updates.json
index 7608ac039..a0ea5cd15 100644
--- a/builds/updates.json
+++ b/builds/updates.json
@@ -3,7 +3,7 @@
"4chan-x@4chan-x.net": {
"updates": [
{
- "version": "1.14.5.13",
+ "version": "1.14.7.2",
"update_link": "https://www.4chan-x.net/builds/4chan-X.crx"
}
]
diff --git a/builds/updates.xml b/builds/updates.xml
index 3f9605aaf..ab0816572 100644
--- a/builds/updates.xml
+++ b/builds/updates.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/crx-chromium-version.txt b/crx-chromium-version.txt
new file mode 100644
index 000000000..2d4f0bd7a
--- /dev/null
+++ b/crx-chromium-version.txt
@@ -0,0 +1 @@
+Chromium 73.0.3683.75 built on Debian buster/sid, running on Debian buster/sid
diff --git a/index.html b/index.html
index ff908ea78..af249ddc2 100644
--- a/index.html
+++ b/index.html
@@ -26,7 +26,7 @@
Private browsing : By default, 4chan X remembers your last read post in a thread and which posts were made by you, even if you are in private browsing / incognito mode. If you want to turn this off, uncheck the Remember Last Read Post and Remember Your Posts options in the settings panel. You can clear all 4chan browsing history saved by 4chan X by resetting your settings.
Install
Chromium
Userscript : Install Violentmonkey ) or Tampermonkey , then click here to install 4chan X .
diff --git a/src/meta/npm-shrinkwrap.json b/package-lock.json
similarity index 66%
rename from src/meta/npm-shrinkwrap.json
rename to package-lock.json
index 0f162b3fb..6299abc20 100644
--- a/src/meta/npm-shrinkwrap.json
+++ b/package-lock.json
@@ -4,31 +4,34 @@
"lockfileVersion": 1,
"dependencies": {
"ajv": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz",
- "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=",
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+ "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
"dev": true,
"requires": {
- "co": "4.6.0",
- "fast-deep-equal": "1.0.0",
- "json-schema-traverse": "0.3.1",
- "json-stable-stringify": "1.0.1"
+ "fast-deep-equal": "2.0.1",
+ "fast-json-stable-stringify": "2.0.0",
+ "json-schema-traverse": "0.4.1",
+ "uri-js": "4.2.2"
}
},
"argparse": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
- "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "1.0.3"
}
},
"asn1": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
- "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
- "dev": true
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": "2.1.2"
+ }
},
"assert-plus": {
"version": "1.0.0",
@@ -49,9 +52,9 @@
"dev": true
},
"aws4": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
- "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"balanced-match": {
@@ -61,11 +64,10 @@
"dev": true
},
"bcrypt-pbkdf": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
- "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
- "optional": true,
"requires": {
"tweetnacl": "0.14.5"
}
@@ -76,19 +78,10 @@
"integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=",
"dev": true
},
- "boom": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
- "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
- "dev": true,
- "requires": {
- "hoek": "4.2.0"
- }
- },
"brace-expansion": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
- "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
@@ -108,15 +101,9 @@
"dev": true,
"requires": {
"exit": "0.1.2",
- "glob": "7.1.2"
+ "glob": "7.1.3"
}
},
- "co": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
- "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
- "dev": true
- },
"coffee-script": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.9.3.tgz",
@@ -124,9 +111,9 @@
"dev": true
},
"combined-stream": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
- "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+ "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"dev": true,
"requires": {
"delayed-stream": "1.0.0"
@@ -147,38 +134,12 @@
"date-now": "0.1.4"
}
},
- "core-js": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
- "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=",
- "dev": true
- },
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
- "cryptiles": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
- "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
- "dev": true,
- "requires": {
- "boom": "5.2.0"
- },
- "dependencies": {
- "boom": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
- "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
- "dev": true,
- "requires": {
- "hoek": "4.2.0"
- }
- }
- }
- },
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -201,33 +162,27 @@
"dev": true
},
"dom-serializer": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
- "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
+ "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
"dev": true,
"requires": {
- "domelementtype": "1.1.3",
- "entities": "1.1.1"
+ "domelementtype": "1.3.1",
+ "entities": "1.1.2"
},
"dependencies": {
- "domelementtype": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
- "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
- "dev": true
- },
"entities": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
- "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
}
}
},
"domelementtype": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
- "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"dev": true
},
"domhandler": {
@@ -236,7 +191,7 @@
"integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=",
"dev": true,
"requires": {
- "domelementtype": "1.3.0"
+ "domelementtype": "1.3.1"
}
},
"domutils": {
@@ -245,18 +200,18 @@
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"dev": true,
"requires": {
- "dom-serializer": "0.1.0",
- "domelementtype": "1.3.0"
+ "dom-serializer": "0.1.1",
+ "domelementtype": "1.3.1"
}
},
"ecc-jsbn": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
- "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"dev": true,
- "optional": true,
"requires": {
- "jsbn": "0.1.1"
+ "jsbn": "0.1.1",
+ "safer-buffer": "2.1.2"
}
},
"entities": {
@@ -265,16 +220,10 @@
"integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=",
"dev": true
},
- "es6-promise": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
- "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
- "dev": true
- },
"esprima": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
- "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
},
"exit": {
@@ -284,9 +233,9 @@
"dev": true
},
"extend": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
- "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
"extsprintf": {
@@ -296,9 +245,15 @@
"dev": true
},
"fast-deep-equal": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
- "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
"dev": true
},
"font-awesome": {
@@ -314,14 +269,14 @@
"dev": true
},
"form-data": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
- "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"dev": true,
"requires": {
"asynckit": "0.4.0",
- "combined-stream": "1.0.5",
- "mime-types": "2.1.17"
+ "combined-stream": "1.0.7",
+ "mime-types": "2.1.22"
}
},
"fs.realpath": {
@@ -340,9 +295,9 @@
}
},
"glob": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -360,40 +315,22 @@
"dev": true
},
"har-validator": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
- "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"dev": true,
"requires": {
- "ajv": "5.2.3",
+ "ajv": "6.10.0",
"har-schema": "2.0.0"
}
},
- "hawk": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
- "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
- "dev": true,
- "requires": {
- "boom": "4.3.1",
- "cryptiles": "3.1.2",
- "hoek": "4.2.0",
- "sntp": "2.0.2"
- }
- },
- "hoek": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
- "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==",
- "dev": true
- },
"htmlparser2": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
"integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=",
"dev": true,
"requires": {
- "domelementtype": "1.3.0",
+ "domelementtype": "1.3.1",
"domhandler": "2.3.0",
"domutils": "1.5.1",
"entities": "1.0.0",
@@ -408,7 +345,7 @@
"requires": {
"assert-plus": "1.0.0",
"jsprim": "1.4.1",
- "sshpk": "1.13.1"
+ "sshpk": "1.16.1"
}
},
"immediate": {
@@ -455,20 +392,19 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
- "dev": true,
- "optional": true
+ "dev": true
},
"jshint": {
- "version": "2.9.5",
- "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.5.tgz",
- "integrity": "sha1-HnJSkVzmgbQIJ+4UJIxG006apiw=",
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz",
+ "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==",
"dev": true,
"requires": {
"cli": "1.0.1",
"console-browserify": "1.1.0",
"exit": "0.1.2",
"htmlparser2": "3.8.3",
- "lodash": "3.7.0",
+ "lodash": "4.17.11",
"minimatch": "3.0.4",
"shelljs": "0.3.0",
"strip-json-comments": "1.0.4"
@@ -481,32 +417,17 @@
"dev": true
},
"json-schema-traverse": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
- "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
- "json-stable-stringify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
- "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
- "dev": true,
- "requires": {
- "jsonify": "0.0.0"
- }
- },
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
- "jsonify": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
- "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
- "dev": true
- },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -520,16 +441,15 @@
}
},
"jszip": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.4.tgz",
- "integrity": "sha512-z6w8iYIxZ/fcgul0j/OerkYnkomH8BZigvzbxVmr2h5HkZUrPtk2kjYtLkqR9wwQxEP6ecKNoKLsbhd18jfnGA==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz",
+ "integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==",
"dev": true,
"requires": {
- "core-js": "2.3.0",
- "es6-promise": "3.0.2",
- "lie": "3.1.1",
- "pako": "1.0.6",
- "readable-stream": "2.0.6"
+ "lie": "3.3.0",
+ "pako": "1.0.10",
+ "readable-stream": "2.3.6",
+ "set-immediate-shim": "1.0.1"
},
"dependencies": {
"isarray": {
@@ -539,43 +459,53 @@
"dev": true
},
"readable-stream": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
- "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
- "process-nextick-args": "1.0.7",
- "string_decoder": "0.10.31",
+ "process-nextick-args": "2.0.0",
+ "safe-buffer": "5.1.2",
+ "string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
}
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
}
}
},
"lie": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
- "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"requires": {
"immediate": "3.0.6"
}
},
"linkify-it": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
- "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz",
+ "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==",
"dev": true,
"requires": {
- "uc.micro": "1.0.3"
+ "uc.micro": "1.0.6"
}
},
"lodash": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz",
- "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=",
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"lodash._reinterpolate": {
@@ -604,22 +534,22 @@
}
},
"markdown-it": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.0.tgz",
- "integrity": "sha512-tNuOCCfunY5v5uhcO2AUMArvKAyKMygX8tfup/JrgnsDqcCATQsAExBq7o5Ml9iMmO82bk6jYNLj6khcrl0JGA==",
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
+ "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
"dev": true,
"requires": {
- "argparse": "1.0.9",
- "entities": "1.1.1",
- "linkify-it": "2.0.3",
+ "argparse": "1.0.10",
+ "entities": "1.1.2",
+ "linkify-it": "2.1.0",
"mdurl": "1.0.1",
- "uc.micro": "1.0.3"
+ "uc.micro": "1.0.6"
},
"dependencies": {
"entities": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
- "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
}
}
@@ -640,18 +570,18 @@
"dev": true
},
"mime-db": {
- "version": "1.30.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
- "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=",
+ "version": "1.38.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
+ "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==",
"dev": true
},
"mime-types": {
- "version": "2.1.17",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
- "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
+ "version": "2.1.22",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
+ "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
"dev": true,
"requires": {
- "mime-db": "1.30.0"
+ "mime-db": "1.38.0"
}
},
"minimatch": {
@@ -660,22 +590,13 @@
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
- "brace-expansion": "1.1.8"
- }
- },
- "node-rsa": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-0.4.2.tgz",
- "integrity": "sha1-1jkXKewWqDDtWjgEKzFX0tXXJTA=",
- "dev": true,
- "requires": {
- "asn1": "0.2.3"
+ "brace-expansion": "1.1.11"
}
},
"oauth-sign": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
- "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"dev": true
},
"once": {
@@ -694,9 +615,9 @@
"dev": true
},
"pako": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
- "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
+ "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==",
"dev": true
},
"path-is-absolute": {
@@ -712,27 +633,33 @@
"dev": true
},
"process-nextick-args": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
- "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+ "dev": true
+ },
+ "psl": {
+ "version": "1.1.31",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
+ "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==",
"dev": true
},
"punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"q": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/q/-/q-1.5.0.tgz",
- "integrity": "sha1-3QG6ydBtMObyGa7LglPunr3DCPE=",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
"dev": true
},
"qs": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
- "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"readable-stream": {
@@ -748,33 +675,31 @@
}
},
"request": {
- "version": "2.83.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz",
- "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==",
+ "version": "2.88.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+ "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"dev": true,
"requires": {
"aws-sign2": "0.7.0",
- "aws4": "1.6.0",
+ "aws4": "1.8.0",
"caseless": "0.12.0",
- "combined-stream": "1.0.5",
- "extend": "3.0.1",
+ "combined-stream": "1.0.7",
+ "extend": "3.0.2",
"forever-agent": "0.6.1",
- "form-data": "2.3.1",
- "har-validator": "5.0.3",
- "hawk": "6.0.2",
+ "form-data": "2.3.3",
+ "har-validator": "5.1.3",
"http-signature": "1.2.0",
"is-typedarray": "1.0.0",
"isstream": "0.1.2",
"json-stringify-safe": "5.0.1",
- "mime-types": "2.1.17",
- "oauth-sign": "0.8.2",
+ "mime-types": "2.1.22",
+ "oauth-sign": "0.9.0",
"performance-now": "2.1.0",
- "qs": "6.5.1",
- "safe-buffer": "5.1.1",
- "stringstream": "0.0.5",
- "tough-cookie": "2.3.3",
+ "qs": "6.5.2",
+ "safe-buffer": "5.1.2",
+ "tough-cookie": "2.4.3",
"tunnel-agent": "0.6.0",
- "uuid": "3.1.0"
+ "uuid": "3.3.2"
}
},
"request-promise": {
@@ -784,22 +709,26 @@
"dev": true,
"requires": {
"bluebird": "2.11.0",
- "lodash": "4.17.4",
- "request": "2.83.0"
- },
- "dependencies": {
- "lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
- "dev": true
- }
+ "lodash": "4.17.11",
+ "request": "2.88.0"
}
},
"safe-buffer": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
- "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "set-immediate-shim": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+ "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"dev": true
},
"shelljs": {
@@ -808,15 +737,6 @@
"integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=",
"dev": true
},
- "sntp": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
- "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
- "dev": true,
- "requires": {
- "hoek": "4.2.0"
- }
- },
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -824,18 +744,19 @@
"dev": true
},
"sshpk": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
- "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+ "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
"dev": true,
"requires": {
- "asn1": "0.2.3",
+ "asn1": "0.2.4",
"assert-plus": "1.0.0",
- "bcrypt-pbkdf": "1.0.1",
+ "bcrypt-pbkdf": "1.0.2",
"dashdash": "1.14.1",
- "ecc-jsbn": "0.1.1",
+ "ecc-jsbn": "0.1.2",
"getpass": "0.1.7",
"jsbn": "0.1.1",
+ "safer-buffer": "2.1.2",
"tweetnacl": "0.14.5"
}
},
@@ -851,12 +772,6 @@
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
},
- "stringstream": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
- "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
- "dev": true
- },
"strip-json-comments": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
@@ -864,12 +779,21 @@
"dev": true
},
"tough-cookie": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
- "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"dev": true,
"requires": {
+ "psl": "1.1.31",
"punycode": "1.4.1"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+ "dev": true
+ }
}
},
"tunnel-agent": {
@@ -878,22 +802,30 @@
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"dev": true,
"requires": {
- "safe-buffer": "5.1.1"
+ "safe-buffer": "5.1.2"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
- "dev": true,
- "optional": true
+ "dev": true
},
"uc.micro": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.3.tgz",
- "integrity": "sha1-ftUNXg+an7ClczeSWfKndFjVAZI=",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"dev": true
},
+ "uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "dev": true,
+ "requires": {
+ "punycode": "2.1.1"
+ }
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -901,9 +833,9 @@
"dev": true
},
"uuid": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
- "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
"dev": true
},
"verror": {
@@ -923,11 +855,11 @@
"integrity": "sha1-aVfXgSzXlgZDAU0Fea+Y3HakPow=",
"dev": true,
"requires": {
- "glob": "7.1.2",
+ "glob": "7.1.3",
"lodash": "2.4.2",
"open": "0.0.5",
- "q": "1.5.0",
- "request": "2.83.0",
+ "q": "1.5.1",
+ "request": "2.88.0",
"request-promise": "2.0.1"
},
"dependencies": {
diff --git a/package.json b/package.json
index 1d43838c4..87076441e 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,6 @@
"recaptchaKey": "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc",
"youtubeAPIKey": "AIzaSyB5_zaen_-46Uhz1xGR-lz1YoUMHqCD6CE",
"distBranch": "gh-pages",
- "uploadPath": "www.4chan-x.net:/var/www/html/",
"includes_only": [
"*://boards.4chan.org/*",
"*://sys.4chan.org/*",
@@ -85,15 +84,14 @@
},
"devDependencies": {
"coffee-script": "=1.9.3",
- "esprima": "^4.0.0",
+ "esprima": "^4.0.1",
"font-awesome": "=4.6.3",
- "jshint": "^2.9.5",
- "jszip": "^3.1.4",
+ "jshint": "^2.10.2",
+ "jszip": "^3.2.1",
"lodash.template": "^4.4.0",
- "markdown-it": "^8.4.0",
+ "markdown-it": "^8.4.2",
"markdown-it-anchor": "^4.0.0",
- "node-rsa": "^0.4.2",
- "request": "^2.83.0",
+ "request": "^2.88.0",
"webstore-upload": "0.0.7"
},
"repository": {
diff --git a/redirect.html b/redirect.html
deleted file mode 100644
index 98360f04e..000000000
--- a/redirect.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<% url = (url || 'https://www.4chan.org/feedback'); %>
-
-
-
-
Redirect
-
-
-Redirecting to
<%= url %> ...
-
-
diff --git a/src/Archive/Redirect.coffee b/src/Archive/Redirect.coffee
index 03d20cfab..c7ec71f3d 100644
--- a/src/Archive/Redirect.coffee
+++ b/src/Archive/Redirect.coffee
@@ -68,7 +68,8 @@ Redirect =
continue
load(i).call {status: 200, response}
else
- CrossOrigin.json url, load(i), true
+ CrossOrigin.ajax url,
+ onloadend: load(i)
else
Redirect.parse [], cb
return
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index 80df4cffd..43ceeabc1 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -7,32 +7,23 @@ Filter =
unless Conf['Filtered Backlinks']
$.addClass doc, 'hide-backlinks'
- nsfwBoards = BoardConfig.sfwBoards(false).join(',')
- sfwBoards = BoardConfig.sfwBoards(true).join(',')
-
for key of Config.filter
for line in Conf[key].split '\n'
continue if line[0] is '#'
- if not (regexp = line.match /\/(.+)\/(\w*)/)
+ if not (regexp = line.match /\/(.*)\/(\w*)/)
continue
# Don't mix up filter flags with the regular expression.
filter = line.replace regexp[0], ''
- # Comma-separated list of the boards this filter applies to.
- # Defaults to global.
- boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global'
- boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards)
- boards = if boards is 'global' then null else boards.split(',')
+ # List of the boards this filter applies to.
+ boards = @parseBoards filter.match(/(?:^|;)\s*boards:([^;]+)/)?[1]
- # boards to exclude from an otherwise global rule
- # due to the sfw and nsfw keywords, also works on all filters
- # replaces 'nsfw' and 'sfw' for consistency
- excludes = filter.match(/exclude:([^;]+)/)?[1].toLowerCase() or null
- excludes = if excludes is null then null else excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',')
+ # Boards to exclude from an otherwise global rule.
+ excludes = @parseBoards filter.match(/(?:^|;)\s*exclude:([^;]+)/)?[1]
- if key in ['uniqueID', 'MD5']
+ if (isstring = (key in ['uniqueID', 'MD5']))
# MD5 filter will use strings instead of regular expressions.
regexp = regexp[1]
else
@@ -50,13 +41,17 @@ Filter =
], 60
continue
- # Filter OPs along with their threads, replies only, or both.
- # Defaults to both.
- op = filter.match(/[^t]op:(yes|no|only)/)?[1] or 'yes'
+ # Filter OPs along with their threads or replies only.
+ op = filter.match(/(?:^|;)\s*op:(no|only)/)?[1] or ''
+ mask = {'no': 1, 'only': 2}[op] or 0
+
+ # Filter only posts with/without files.
+ file = filter.match(/(?:^|;)\s*file:(no|only)/)?[1] or ''
+ mask = mask | ({'no': 4, 'only': 8}[file] or 0)
# Overrule the `Show Stubs` setting.
# Defaults to stub showing.
- stub = switch filter.match(/stub:(yes|no)/)?[1]
+ stub = switch filter.match(/(?:^|;)\s*stub:(yes|no)/)?[1]
when 'yes'
true
when 'no'
@@ -64,25 +59,29 @@ Filter =
else
Conf['Stubs']
- # Highlight the post, or hide it.
+ # Desktop notification
+ noti = /(?:^|;)\s*notify/.test filter
+
+ # Highlight the post.
# If not specified, the highlight class will be filter-highlight.
- # Defaults to post hiding.
- if hl = /highlight/.test filter
- hl = filter.match(/highlight:([\w-]+)/)?[1] or 'filter-highlight'
+ if (hl = /(?:^|;)\s*highlight/.test filter)
+ hl = filter.match(/(?:^|;)\s*highlight:([\w-]+)/)?[1] or 'filter-highlight'
# Put highlighted OP's thread on top of the board page or not.
# Defaults to on top.
- top = filter.match(/top:(yes|no)/)?[1] or 'yes'
+ top = filter.match(/(?:^|;)\s*top:(yes|no)/)?[1] or 'yes'
top = top is 'yes' # Turn it into a boolean
# Fields that this filter applies to (for 'general' filters)
if key is 'general'
if (types = filter.match /(?:^|;)\s*type:([^;]*)/)
- types = types[1].split(',').filter (x) ->
- x of Config.filter and x isnt 'general'
+ types = types[1].split(',')
else
types = ['subject', 'name', 'filename', 'comment']
- filter = @createFilter regexp, boards, excludes, op, stub, hl, top
+ # Hide the post (default case).
+ hide = !(hl or noti)
+
+ filter = {isstring, regexp, boards, excludes, mask, hide, stub, hl, top, noti}
if key is 'general'
for type in types
(@filters[type] or= []).push filter
@@ -94,30 +93,27 @@ Filter =
name: 'Filter'
cb: @node
- createFilter: (regexp, boards, excludes, op, stub, hl, top) ->
- test =
- if typeof regexp is 'string'
- # MD5 checking
- (value) -> regexp is value
- else
- (value) -> regexp.test value
+ # Parse comma-separated list of boards.
+ # Sites can be specified by a beginning part of the site domain followed by a colon.
+ parseBoards: (boardsRaw) ->
+ return false unless boardsRaw
+ return boards if (boards = Filter.parseBoardsMemo[boardsRaw])
+ boards = {}
+ siteFilter = ''
+ for boardID in boardsRaw.split(',')
+ if ':' in boardID
+ [siteFilter, boardID] = boardID.split(':')[-2..]
+ for siteID, siteProperties of Conf['siteProperties']
+ continue if siteProperties.canonical or siteID[...siteFilter.length] isnt siteFilter
+ if boardID in ['nsfw', 'sfw']
+ for boardID2 in SW[siteProperties.software]?.sfwBoards?(boardID is 'sfw') or []
+ boards["#{siteID}/#{boardID2}"] = true
+ else
+ boards["#{siteID}/#{encodeURIComponent boardID}"] = true
+ Filter.parseBoardsMemo[boardsRaw] = boards
+ boards
- settings =
- hide: !hl
- stub: stub
- class: hl
- top: top
-
- (value, boardID, isReply) ->
- if boards and boardID not in boards
- return false
- if excludes and boardID in excludes
- return false
- if isReply and op is 'only' or !isReply and op is 'no'
- return false
- unless test value
- return false
- settings
+ parseBoardsMemo: {}
test: (post, hideable=true) ->
return post.filterResults if post.filterResults
@@ -125,27 +121,40 @@ Filter =
stub = true
hl = undefined
top = false
+ noti = false
if QuoteYou.isYou(post)
hideable = false
- for key of Filter.filters when ((value = Filter[key] post)?)
+ mask = (if post.isReply then 2 else 1)
+ mask = (mask | (if post.file then 4 else 8))
+ board = "#{post.siteID}/#{post.boardID}"
+ site = "#{post.siteID}/*"
+ for key of Filter.filters when ((value = Filter.value key, post)?)
# Continue if there's nothing to filter (no tripcode for example).
- for filter in Filter.filters[key] when (result = filter value, post.boardID, post.isReply)
- if result.hide
+ for filter in Filter.filters[key]
+ continue if (
+ (filter.boards and !(filter.boards[board] or filter.boards[site] )) or
+ (filter.excludes and (filter.excludes[board] or filter.excludes[site])) or
+ (filter.mask & mask) or
+ (if filter.isstring then (filter.regexp isnt value) else !filter.regexp.test(value))
+ )
+ if filter.hide
if hideable
hide = true
- stub and= result.stub
+ stub and= filter.stub
else
- unless hl and result.class in hl
- (hl or= []).push result.class
- top or= result.top
+ unless hl and filter.hl in hl
+ (hl or= []).push filter.hl
+ top or= filter.top
+ if filter.noti
+ noti = true
if hide
{hide, stub}
else
- {hl, top}
+ {hl, top, noti}
node: ->
return if @isClone
- {hide, stub, hl, top} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index'))
+ {hide, stub, hl, top, noti} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index'))
if hide
if @isReply
PostHiding.hide @, stub
@@ -155,24 +164,33 @@ Filter =
if hl
@highlights = hl
$.addClass @nodes.root, hl...
- return
+ if noti and Unread.posts and (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@)
+ Unread.openNotification @, ' triggered a notification filter'
isHidden: (post) ->
!!Filter.test(post).hide
- postID: (post) -> "#{post.ID}"
- name: (post) -> post.info.name
- uniqueID: (post) -> post.info.uniqueID
- tripcode: (post) -> post.info.tripcode
- capcode: (post) -> post.info.capcode
- pass: (post) -> post.info.pass
- subject: (post) -> post.info.subject or (if post.isReply then undefined else '')
- comment: (post) -> (post.info.comment ?= Build.parseComment post.info.commentHTML.innerHTML)
- flag: (post) -> post.info.flag
- filename: (post) -> post.file?.name
- dimensions: (post) -> post.file?.dimensions
- filesize: (post) -> post.file?.size
- MD5: (post) -> post.file?.MD5
+ valueF:
+ postID: (post) -> "#{post.ID}"
+ name: (post) -> post.info.name
+ uniqueID: (post) -> post.info.uniqueID or ''
+ tripcode: (post) -> post.info.tripcode
+ capcode: (post) -> post.info.capcode
+ pass: (post) -> post.info.pass
+ email: (post) -> post.info.email
+ subject: (post) -> post.info.subject or (if post.isReply then undefined else '')
+ comment: (post) -> (post.info.comment ?= Build.parseComment post.info.commentHTML.innerHTML)
+ flag: (post) -> post.info.flag
+ filename: (post) -> post.file?.name
+ dimensions: (post) -> post.file?.dimensions
+ filesize: (post) -> post.file?.size
+ MD5: (post) -> post.file?.MD5
+
+ value: (key, post) ->
+ if key of Filter.valueF
+ Filter.valueF[key](post)
+ else
+ key.split('+').map((k) -> Filter.valueF[k]?(post) or '').join('\n')
addFilter: (type, re, cb) ->
$.get type, Conf[type], (item) ->
@@ -245,6 +263,7 @@ Filter =
['Tripcode', 'tripcode']
['Capcode', 'capcode']
['Pass Date', 'pass']
+ ['Email', 'email']
['Subject', 'subject']
['Comment', 'comment']
['Flag', 'flag']
@@ -268,14 +287,14 @@ Filter =
return {
el: el
open: (post) ->
- value = Filter[type] post
+ value = Filter.value type, post
value?
}
makeFilter: ->
{type} = @dataset
# Convert value -> regexp, unless type is MD5
- value = Filter[type] Filter.menu.post
+ value = Filter.value type, Filter.menu.post
re = if type in ['uniqueID', 'MD5'] then value else Filter.escape(value)
re = if type in ['uniqueID', 'MD5']
"/#{re}/"
diff --git a/src/General/BoardConfig.coffee b/src/General/BoardConfig.coffee
index fb8ee4f06..842cd84da 100644
--- a/src/General/BoardConfig.coffee
+++ b/src/General/BoardConfig.coffee
@@ -48,6 +48,11 @@ BoardConfig =
domain: (board) ->
"boards.#{if BoardConfig.isSFW(board) then '4channel' else '4chan'}.org"
+ isArchived: (board) ->
+ # assume archive exists if no data available to prevent cleaning of archived threads
+ data = (@boards or Conf['boardConfig'].boards)[board]
+ !data or data.is_archived
+
noAudio: (boardID) ->
return false unless Site.software is 'yotsuba'
boards = @boards or Conf['boardConfig'].boards
diff --git a/src/General/Build.Test.coffee b/src/General/Build.Test.coffee
index cc015ac11..24ba74e57 100644
--- a/src/General/Build.Test.coffee
+++ b/src/General/Build.Test.coffee
@@ -65,7 +65,8 @@ Build.Test =
testOne: (post) ->
Build.Test.postsRemaining++
- $.cache "#{location.protocol}//a.4cdn.org/#{post.board.ID}/thread/#{post.thread.ID}.json", ->
+ $.cache Site.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), ->
+ return unless @response
{posts} = @response
Build.spoilerRange[post.board.ID] = posts[0].custom_spoiler
for postData in posts
@@ -90,8 +91,8 @@ Build.Test =
c.log y.outerHTML
for key of Config.filter when not key is 'General' and not (key is 'MD5' and post.board.ID is 'f')
- val1 = Filter[key] obj
- val2 = Filter[key] post2
+ val1 = Filter.value key, obj
+ val2 = Filter.value key, post2
if val1 isnt val2
fail = true
c.log "#{post.fullID} has filter bug in #{key}"
diff --git a/src/General/Build.coffee b/src/General/Build.coffee
index ccf890dc1..1bde00fa7 100644
--- a/src/General/Build.coffee
+++ b/src/General/Build.coffee
@@ -25,18 +25,24 @@ Build =
sameThread: (boardID, threadID) ->
g.VIEW is 'thread' and g.BOARD.ID is boardID and g.THREADID is +threadID
- postURL: (boardID, threadID, postID) ->
- if Build.sameThread boardID, threadID
- "#p#{postID}"
+ threadURL: (boardID, threadID) ->
+ if boardID isnt g.BOARD.ID
+ "//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}"
+ else if g.VIEW isnt 'thread' or +threadID isnt g.THREADID
+ "/#{boardID}/thread/#{threadID}"
else
- "/#{boardID}/thread/#{threadID}#p#{postID}"
+ ''
- parseJSON: (data, boardID) ->
+ postURL: (boardID, threadID, postID) ->
+ "#{Build.threadURL(boardID, threadID)}#p#{postID}"
+
+ parseJSON: (data, boardID, siteID) ->
o =
# id
ID: data.no
threadID: data.resto or data.no
boardID: boardID
+ siteID: siteID or Site.hostname
isReply: !!data.resto
# thread status
isSticky: !!data.sticky
@@ -44,7 +50,6 @@ Build =
isArchived: !!data.archived
# file status
fileDeleted: !!data.filedeleted
- xa18: data.xa18
o.info =
subject: Build.unescape data.sub
email: Build.unescape data.email
@@ -80,6 +85,9 @@ Build =
tag: data.tag
hasDownscale: !!data.m_img
o.file.dimensions = "#{o.file.width}x#{o.file.height}" unless /\.pdf$/.test o.file.url
+ # Temporary JSON properties for events such as April 1 / Halloween
+ for key of data when key[0] is 'x'
+ o[key] = data[key]
o
parseComment: (html) ->
@@ -124,11 +132,12 @@ Build =
capcodePlural = "#{capcodeLong}s"
capcodeDescription = "a 4chan #{capcodeLong}"
- postLink = Build.postURL boardID, threadID, ID
+ url = Build.threadURL boardID, threadID
+ postLink = "#{url}#p#{ID}"
quoteLink = if Build.sameThread boardID, threadID
"javascript:quote('#{+ID}');"
else
- "/#{boardID}/thread/#{threadID}#q#{ID}"
+ "#{url}#q#{ID}"
postInfo = <%= readHTML('PostInfo.html') %>
@@ -156,12 +165,12 @@ Build =
# Fix quotelinks
for quote in $$ '.quotelink', container
href = quote.getAttribute 'href'
- if (href[0] is '#') and !(Build.sameThread boardID, threadID)
- quote.href = ("/#{boardID}/thread/#{threadID}") + href
- else if (match = href.match /^\/([^\/]+)\/thread\/(\d+)/) and (Build.sameThread match[1], match[2])
- quote.href = href.match(/(#[^#]*)?$/)[0] or '#'
- else if /^\d+(#|$)/.test(href) and not (g.VIEW is 'thread' and g.BOARD.ID is boardID) # used on /f/
- quote.href = "/#{boardID}/thread/#{href}"
+ if (href[0] is '#')
+ if !Build.sameThread(boardID, threadID)
+ quote.href = Build.threadURL(boardID, threadID) + href
+ else
+ if (match = quote.href.match SW.yotsuba.regexp.quotelink) and (Build.sameThread match[1], match[2])
+ quote.href = href.match(/(#[^#]*)?$/)[0] or '#'
container
diff --git a/src/General/Build/PostInfo.html b/src/General/Build/PostInfo.html
index 8a9823ad5..fc1a8224a 100644
--- a/src/General/Build/PostInfo.html
+++ b/src/General/Build/PostInfo.html
@@ -5,9 +5,9 @@
?{email}{
}
${name}
?{tripcode}{ ${tripcode} }
+ ?{o.xa19s}{ ${o.xa19s} }
?{pass}{ }
?{capcode}{ ## ${capcode} }
- ?{!capcode && typeof o.xa18 !== "undefined"}{ }
?{email}{ }
?{boardID === "f" && !o.isReply || capcodeDescription}{}{ }
?{capcodeDescription}{
}
@@ -19,6 +19,7 @@
No.
${ID}
+ ?{o.xa19l && o.isReply}{ Like! ×${o.xa19l} }
?{o.isSticky}{ }
?{o.isClosed && !o.isArchived}{ }
?{o.isArchived}{ }
diff --git a/src/General/Header.coffee b/src/General/Header.coffee
index 265f804bd..2cd3060f2 100644
--- a/src/General/Header.coffee
+++ b/src/General/Header.coffee
@@ -282,7 +282,7 @@ Header =
return a.firstChild # Its text node.
if /-expired/.test t
- if boardID not in ['b', 'f', 'trash', 'bant']
+ if BoardConfig.isArchived(boardID)
a.href = "//#{BoardConfig.domain(boardID)}/#{boardID}/archive"
else
return a.firstChild # Its text node.
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index 8acf0e7bc..6f72b0c92 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -84,7 +84,7 @@ Index =
@navLinks = $.el 'div', className: 'navLinks json-index'
$.extend @navLinks, <%= readHTML('NavLinks.html') %>
$('.cataloglink a', @navLinks).href = CatalogLinks.catalog()
- $('.archlistlink', @navLinks).hidden = true if g.BOARD.ID in ['b', 'trash', 'bant']
+ $('.archlistlink', @navLinks).hidden = true unless BoardConfig.isArchived(g.BOARD.ID)
$.on $('#index-last-refresh a', @navLinks), 'click', @cb.refreshFront
# Search field
@@ -573,48 +573,43 @@ Index =
"#{hiddenCount} hidden threads"
update: (firstTime) ->
- Index.req?.abort()
- Index.notice?.close()
+ if (oldReq = Index.req)
+ delete Index.req
+ oldReq.abort()
- if Conf['Index Refresh Notifications'] and d.readyState isnt 'loading'
+ if Conf['Index Refresh Notifications']
# Optional notification for manual refreshes
- Index.notice = new Notice 'info', 'Refreshing index...'
+ Index.notice or= new Notice 'info', 'Refreshing index...'
else
# Also display notice if Index Refresh is taking too long
- now = Date.now()
- $.ready ->
- Index.nTimeout = setTimeout (->
- if Index.req and !Index.notice
- Index.notice = new Notice 'info', 'Refreshing index...'
- ), 3 * $.SECOND - (Date.now() - now)
+ Index.nTimeout or= setTimeout ->
+ Index.notice or= new Notice 'info', 'Refreshing index...'
+ , 3 * $.SECOND
# Hard refresh in case of incomplete page load.
if not firstTime and d.readyState isnt 'loading' and not $('.board + *')
location.reload()
return
- Index.req = $.ajax "#{location.protocol}//a.4cdn.org/#{g.BOARD}/catalog.json",
- onabort: Index.load
- onloadend: Index.load
- ,
- whenModified: 'Index'
+ Index.req = $.whenModified(
+ Site.urls.catalogJSON({boardID: g.BOARD.ID}),
+ 'Index',
+ Index.load
+ )
$.addClass Index.button, 'fa-spin'
- load: (e) ->
+ load: ->
+ return if @ isnt Index.req # aborted
+
$.rmClass Index.button, 'fa-spin'
- {req, notice, nTimeout} = Index
+ {notice, nTimeout} = Index
clearTimeout nTimeout if nTimeout
delete Index.nTimeout
delete Index.req
delete Index.notice
- if e.type is 'abort'
- req.onloadend = null
- notice?.close()
- return
-
- if req.status not in [200, 304]
- err = "Index refresh failed. #{if req.status then "Error #{req.statusText} (#{req.status})" else 'Connection Error'}"
+ if @status not in [200, 304]
+ err = "Index refresh failed. #{if @status then "Error #{@statusText} (#{@status})" else 'Connection Error'}"
if notice
notice.setType 'warning'
notice.el.lastElementChild.textContent = err
@@ -624,13 +619,12 @@ Index =
return
try
- if req.status is 200
- Index.parse req.response
- else if req.status is 304
+ if @status is 200
+ Index.parse @response
+ else if @status is 304
Index.pageLoad()
catch err
c.error "Index failure: #{err.message}", err.stack
- # network error or non-JSON content for example.
if notice
notice.setType 'error'
notice.el.lastElementChild.textContent = 'Index refresh failed.'
@@ -648,7 +642,7 @@ Index =
notice.close()
timeEl = $ '#index-last-refresh time', Index.navLinks
- timeEl.dataset.utc = Date.parse req.getResponseHeader 'Last-Modified'
+ timeEl.dataset.utc = Date.parse @getResponseHeader 'Last-Modified'
RelativeDates.update timeEl
parse: (pages) ->
diff --git a/src/General/Settings.coffee b/src/General/Settings.coffee
index 8953ebc62..535a3022e 100644
--- a/src/General/Settings.coffee
+++ b/src/General/Settings.coffee
@@ -206,16 +206,20 @@ Settings =
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
export: ->
- # Make sure to export the most recent data.
- $.get Conf, (Conf) ->
+ # Make sure to export the most recent data, but don't overwrite existing `Conf` object.
+ Conf2 = {}
+ $.extend Conf2, Conf
+ $.get Conf2, (Conf2) ->
# Don't export cached JSON data.
- delete Conf['boardConfig']
- (Settings.downloadExport {version: g.VERSION, date: Date.now(), Conf})
+ delete Conf2['boardConfig']
+ (Settings.downloadExport {version: g.VERSION, date: Date.now(), Conf: Conf2})
downloadExport: (data) ->
+ blob = new Blob [JSON.stringify(data, null, 2)], {type: 'application/json'}
+ url = URL.createObjectURL blob
a = $.el 'a',
download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
- href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
+ href: url
p = $ '.imp-exp-result', Settings.dialog
$.rmAll p
$.add p, a
@@ -473,6 +477,12 @@ Settings =
[hostname, software] = line.split(' ')
siteProperties[hostname] = {software}
set 'siteProperties', siteProperties
+ if compareString < '00001.00014.00006.00006'
+ if data['sauces']?
+ set 'sauces', data['sauces'].replace(
+ /\/\/%\$1\.deviantart\.com\/gallery\/#\/d%\$2;regexp:\/\^\\w\+_by_\(\\w\+\)-d\(\[\\da-z\]\+\)\//g,
+ '//www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/'
+ )
changes
loadSettings: (data, cb) ->
diff --git a/src/General/Settings/Filter-guide.html b/src/General/Settings/Filter-guide.html
index df0838609..8a6a3ea84 100644
--- a/src/General/Settings/Filter-guide.html
+++ b/src/General/Settings/Filter-guide.html
@@ -3,20 +3,29 @@
Use regular expressions , one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.
- MD5 filtering uses exact string matching, not regular expressions.
+ MD5 and Unique ID filtering use exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:
Per boards, separate them with commas. It is global if not specified. Use sfw and nsfw to reference all worksafe or not-worksafe boards.
For example: boards:a,jp;.
+ To specify boards on a particular site, put the beginning of the domain and a slash character before the list.
+ Any initial www. should not be included, and all 4chan domains are considered 4chan.org.
+ For example: boards:4:a,jp,sama:a,z;.
+ An asterisk can be used to specify all boards on a site.
+ For example: boards:4:*;.
- In case of a global rule or one that uses sfw/nsfw, select boards to be excluded from the filter.
+ Select boards to be excluded from the filter. The syntax is the same as for the boards: option above.
For example: exclude:vg,v;.
- Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).
- For example: op:only;, op:no; or op:yes;.
+ Filter OPs only along with their threads (`only`) or replies only (`no`).
+ For example: op:only; or op:no;.
+
+
+ Filter only posts with files (`only`) or only posts without files (`no`).
+ For example: file:only; or file:no;.
Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).
@@ -30,10 +39,16 @@
Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
+
+ Show a desktop notification instead of hiding.
+ For example: notify;.
+
Filters in the "General" section apply to multiple fields, by default subject,name,filename,comment.
The fields can be specified with the type option, separated by commas.
- For example: type:@{filterTypes};.
+ For example: type:@{filterTypes};.
+ Types can also be combined with a + sign; this indicates the filter applies to the given fields joined by newlines.
+ For example: type:filename+filesize+dimensions;.
diff --git a/src/General/Settings/Filter-select.html b/src/General/Settings/Filter-select.html
index 1d8184c0a..6632f8258 100644
--- a/src/General/Settings/Filter-select.html
+++ b/src/General/Settings/Filter-select.html
@@ -7,6 +7,7 @@
Tripcode
Capcode
Pass Date
+ Email
Subject
Comment
Flag
diff --git a/src/General/UI.coffee b/src/General/UI.coffee
index c57bc0251..929f85278 100644
--- a/src/General/UI.coffee
+++ b/src/General/UI.coffee
@@ -308,7 +308,8 @@ dragend = ->
$.off d, 'mouseup', @up
$.set "#{@id}.position", @style.cssText
-hoverstart = ({root, el, latestEvent, endEvents, height, cb, noRemove}) ->
+hoverstart = ({root, el, latestEvent, endEvents, height, width, cb, noRemove}) ->
+ rect = root.getBoundingClientRect()
o = {
root
el
@@ -320,7 +321,10 @@ hoverstart = ({root, el, latestEvent, endEvents, height, cb, noRemove}) ->
clientHeight: doc.clientHeight
clientWidth: doc.clientWidth
height
+ width
noRemove
+ clientX: (rect.left + rect.right) / 2
+ clientY: (rect.top + rect.bottom) / 2
}
o.hover = hover.bind o
o.hoverend = hoverend.bind o
@@ -344,7 +348,8 @@ hoverstart.padding = 25
hover = (e) ->
@latestEvent = e
height = (@height or @el.offsetHeight) + hoverstart.padding
- {clientX, clientY} = e
+ width = (@width or @el.offsetWidth)
+ {clientX, clientY} = if Conf['Follow Cursor'] then e else @
top = if @isImage
Math.max 0, clientY * (@clientHeight - height) / @clientHeight
@@ -353,10 +358,10 @@ hover = (e) ->
threshold = @clientWidth / 2
threshold = Math.max threshold, @clientWidth - 400 unless @isImage
- [left, right] = if clientX <= threshold
- [clientX + 45 + 'px', '']
- else
- ['', @clientWidth - clientX + 45 + 'px']
+ marginX = (if clientX <= threshold then clientX else @clientWidth - clientX) + 45
+ marginX = Math.min(marginX, @clientWidth - width) if @isImage
+ marginX += 'px'
+ [left, right] = if clientX <= threshold then [marginX, ''] else ['', marginX]
{style} = @
style.top = top + 'px'
diff --git a/src/Images/ImageCommon.coffee b/src/Images/ImageCommon.coffee
index eec68dafa..b7e0b7242 100644
--- a/src/Images/ImageCommon.coffee
+++ b/src/Images/ImageCommon.coffee
@@ -54,7 +54,7 @@ ImageCommon =
clearTimeout timeoutID if delay?
cb URL
- $.ajax "#{location.protocol}//a.4cdn.org/#{post.board}/thread/#{post.thread}.json", onload: ->
+ $.ajax Site.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), onloadend: ->
post.kill !post.isClone if @status is 404
return redirect() if @status isnt 200
for postObj in @response.posts
diff --git a/src/Images/ImageExpand.coffee b/src/Images/ImageExpand.coffee
index 7ea4b8ac5..717a527f0 100644
--- a/src/Images/ImageExpand.coffee
+++ b/src/Images/ImageExpand.coffee
@@ -250,7 +250,7 @@ ImageExpand =
mouseover: -> mousedown = false
mousedown: (e) -> mousedown = true if e.button is 0
mouseup: (e) -> mousedown = false if e.button is 0
- mouseout: (e) -> ImageExpand.toggle(Get.postFromNode @) if mousedown and e.clientX <= @getBoundingClientRect().left
+ mouseout: (e) -> ImageExpand.toggle(Get.postFromNode @) if ((e.buttons & 1) or mousedown) and e.clientX <= @getBoundingClientRect().left
setupVideoCB: (post) ->
for eventName, cb of ImageExpand.videoCB
diff --git a/src/Images/ImageHover.coffee b/src/Images/ImageHover.coffee
index ee47c120e..d9cd67149 100644
--- a/src/Images/ImageHover.coffee
+++ b/src/Images/ImageHover.coffee
@@ -46,19 +46,22 @@ ImageHover =
if Conf['Autoplay']
el.play()
@currentTime = el.currentTime if @nodeName is 'VIDEO'
- [width, height] = (+x for x in file.dimensions.split 'x')
- {left, right} = @getBoundingClientRect()
- maxWidth = Math.max left, doc.clientWidth - right
- maxHeight = doc.clientHeight - UI.hover.padding
- scale = Math.min 1, maxWidth / width, maxHeight / height
- el.style.maxWidth = "#{scale * width}px"
- el.style.maxHeight = "#{scale * height}px"
+ if file.dimensions
+ [width, height] = (+x for x in file.dimensions.split 'x')
+ maxWidth = doc.clientWidth
+ maxHeight = doc.clientHeight - UI.hover.padding
+ scale = Math.min 1, maxWidth / width, maxHeight / height
+ width *= scale
+ height *= scale
+ el.style.maxWidth = "#{width}px"
+ el.style.maxHeight = "#{height}px"
UI.hover
root: @
el: el
latestEvent: e
endEvents: 'mouseout click'
- height: scale * height
+ height: height
+ width: width
noRemove: true
cb: ->
$.off el, 'error', error
diff --git a/src/Images/Sauce.coffee b/src/Images/Sauce.coffee
index f89e77477..28669ba73 100644
--- a/src/Images/Sauce.coffee
+++ b/src/Images/Sauce.coffee
@@ -57,7 +57,7 @@ Sauce =
parts[key] = parts[key].replace /%(T?URL|IMG|[sh]?MD5|board|name|%|semi|\$\d+)/g, (orig, parameter) ->
if parameter[0] is '$'
return orig unless matches
- type = matches[parameter[1..]]
+ type = matches[parameter[1..]] or ''
else
type = Sauce.formatters[parameter] post, ext
if not type?
diff --git a/src/Linkification/Embedding.coffee b/src/Linkification/Embedding.coffee
index 7de873103..98084a655 100644
--- a/src/Linkification/Embedding.coffee
+++ b/src/Linkification/Embedding.coffee
@@ -111,7 +111,7 @@ Embedding =
if service.queue.length >= service.batchSize
Embedding.flushTitles service
else
- CrossOrigin.json service.api(uid), (-> Embedding.cb.title @, data)
+ CrossOrigin.cache service.api(uid), (-> Embedding.cb.title @, data)
flushTitles: (service) ->
{queue} = service
@@ -120,7 +120,7 @@ Embedding =
cb = ->
Embedding.cb.title @, data for data in queue
return
- CrossOrigin.json service.api(data.uid for data in queue), cb
+ CrossOrigin.cache service.api(data.uid for data in queue), cb
preview: (data) ->
{key, uid, link} = data
@@ -275,7 +275,7 @@ Embedding =
el = $.el 'pre',
hidden: true
id: "gist-embed-#{counter++}"
- CrossOrigin.json "https://api.github.com/gists/#{a.dataset.uid}", ->
+ CrossOrigin.cache "https://api.github.com/gists/#{a.dataset.uid}", ->
el.textContent = Object.values(@response.files)[0].content
el.className = 'prettyprint'
$.global ->
diff --git a/src/Menu/ArchiveLink.coffee b/src/Menu/ArchiveLink.coffee
index e110bb310..c96046e00 100644
--- a/src/Menu/ArchiveLink.coffee
+++ b/src/Menu/ArchiveLink.coffee
@@ -45,7 +45,7 @@ ArchiveLink =
value = if type is 'country'
post.info.flagCode or post.info.flagCodeTroll
else
- Filter[type] post
+ Filter.value type, post
# We want to parse the exact same stuff as the filter does already.
return false unless value
el.href = Redirect.to 'search',
diff --git a/src/Menu/DeleteLink.coffee b/src/Menu/DeleteLink.coffee
index 870fc9ddf..c16a52fa6 100644
--- a/src/Menu/DeleteLink.coffee
+++ b/src/Menu/DeleteLink.coffee
@@ -82,12 +82,15 @@ DeleteLink =
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
responseType: 'document'
withCredentials: true
- onload: -> DeleteLink.load link, post, fileOnly, @response
- onerror: -> DeleteLink.error link, post
- ,
+ onloadend: -> DeleteLink.load link, post, fileOnly, @response
form: $.formData form
load: (link, post, fileOnly, resDoc) ->
+ unless resDoc
+ new Notice 'warning', 'Connection error, please retry.', 20
+ $.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
+ return
+
link.textContent = DeleteLink.linkText fileOnly
if resDoc.title is '4chan - Banned' # Ban/warn check
el = $.el 'span', <%= html('You can't delete posts because you are banned .') %>
@@ -106,10 +109,6 @@ DeleteLink =
(post.origin or post).kill fileOnly
link.textContent = 'Deleted' if post.fullID is DeleteLink.post.fullID
- error: (link, post) ->
- new Notice 'warning', 'Connection error, please retry.', 20
- $.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
-
cooldown:
seconds: {}
diff --git a/src/Miscellaneous/CatalogLinks.coffee b/src/Miscellaneous/CatalogLinks.coffee
index f0ff09e13..8fed1ede7 100644
--- a/src/Miscellaneous/CatalogLinks.coffee
+++ b/src/Miscellaneous/CatalogLinks.coffee
@@ -75,7 +75,7 @@ CatalogLinks =
return
catalog: (board=g.BOARD.ID) ->
- if Conf['External Catalog'] and board in ['a', 'c', 'g', 'biz', 'k', 'm', 'o', 'p', 'v', 'vg', 'vr', 'w', 'wg', 'cm', '3', 'adv', 'an', 'asp', 'cgl', 'ck', 'co', 'diy', 'fa', 'fit', 'gd', 'int', 'jp', 'lit', 'mlp', 'mu', 'n', 'out', 'po', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'vp', 'wsg', 'x', 'f', 'pol', 's4s', 'lgbt']
+ if Conf['External Catalog'] and board in ['3', 'a', 'adv', 'an', 'asp', 'biz', 'c', 'cgl', 'ck', 'cm', 'co', 'diy', 'f', 'fa', 'fit', 'g', 'gd', 'his', 'i', 'int', 'jp', 'k', 'lgbt', 'lit', 'm', 'mlp', 'mu', 'n', 'news', 'o', 'out', 'p', 'po', 'pol', 's4s', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'v', 'vg', 'vip', 'vp', 'vr', 'w', 'wg', 'wsg', 'wsr', 'x']
"//catalog.neet.tv/#{board}/"
else if Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog']
if location.hostname in ['boards.4chan.org', 'boards.4channel.org'] and g.BOARD.ID is board and g.VIEW is 'index' then '#catalog' else "//#{BoardConfig.domain(board)}/#{board}/#catalog"
diff --git a/src/Miscellaneous/ExpandComment.coffee b/src/Miscellaneous/ExpandComment.coffee
index 09d596774..6fbf847e0 100644
--- a/src/Miscellaneous/ExpandComment.coffee
+++ b/src/Miscellaneous/ExpandComment.coffee
@@ -2,9 +2,6 @@ ExpandComment =
init: ->
return if g.VIEW isnt 'index' or !Conf['Comment Expansion'] or Conf['JSON Index']
- @callbacks.push Fourchan.code if g.BOARD.ID is 'g'
- @callbacks.push Fourchan.math if g.BOARD.ID is 'sci'
-
Callbacks.Post.push
name: 'Comment Expansion'
cb: @node
@@ -26,7 +23,7 @@ ExpandComment =
return
return if not (a = $ '.abbr > a', post.nodes.comment)
a.textContent = "Post No.#{post} Loading..."
- $.cache "#{location.protocol}//a.4cdn.org#{a.pathname.split(/\/+/).splice(0,4).join('/')}.json", -> ExpandComment.parse @, a, post
+ $.cache Site.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), -> ExpandComment.parse @, a, post
contract: (post) ->
return unless post.nodes.shortComment
@@ -38,7 +35,7 @@ ExpandComment =
parse: (req, a, post) ->
{status} = req
unless status in [200, 304]
- a.textContent = "Error #{req.statusText} (#{status})"
+ a.textContent = if status then "Error #{req.statusText} (#{status})" else 'Connection Error'
return
posts = req.response.posts
diff --git a/src/Miscellaneous/ExpandThread.coffee b/src/Miscellaneous/ExpandThread.coffee
index d86451371..cf525d00e 100644
--- a/src/Miscellaneous/ExpandThread.coffee
+++ b/src/Miscellaneous/ExpandThread.coffee
@@ -18,7 +18,9 @@ ExpandThread =
disconnect: (refresh) ->
return if g.VIEW is 'thread' or !Conf['Thread Expansion']
for threadID, status of ExpandThread.statuses
- status.req?.abort()
+ if (oldReq = status.req)
+ delete status.req
+ oldReq.abort()
delete ExpandThread.statuses[threadID]
$.off d, 'IndexRefreshInternal', @onIndexRefresh unless refresh
@@ -52,15 +54,17 @@ ExpandThread =
expand: (thread, a) ->
ExpandThread.statuses[thread] = status = {}
a.textContent = Build.summaryText '...', a.textContent.match(/\d+/g)...
- status.req = $.cache "#{location.protocol}//a.4cdn.org/#{thread.board}/thread/#{thread}.json", ->
+ status.req = $.cache Site.urls.threadJSON({boardID: thread.board.ID, threadID: thread.ID}), ->
+ return if @ isnt status.req # aborted
delete status.req
ExpandThread.parse @, thread, a
contract: (thread, a, threadRoot) ->
status = ExpandThread.statuses[thread]
delete ExpandThread.statuses[thread]
- if status.req
- status.req.abort()
+ if (oldReq = status.req)
+ delete status.req
+ oldReq.abort()
a.textContent = Build.summaryText '+', a.textContent.match(/\d+/g)... if a
return
@@ -89,7 +93,7 @@ ExpandThread =
parse: (req, thread, a) ->
if req.status not in [200, 304]
- a.textContent = "Error #{req.statusText} (#{req.status})"
+ a.textContent = if req.status then "Error #{req.statusText} (#{req.status})" else 'Connection Error'
return
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler
diff --git a/src/Miscellaneous/Fourchan.coffee b/src/Miscellaneous/Fourchan.coffee
index 581e0559e..7fba2af7f 100644
--- a/src/Miscellaneous/Fourchan.coffee
+++ b/src/Miscellaneous/Fourchan.coffee
@@ -1,8 +1,11 @@
Fourchan =
init: ->
- return unless g.VIEW in ['index', 'thread', 'archive']
+ return unless Site.software is 'yotsuba' and g.VIEW in ['index', 'thread', 'archive']
+ BoardConfig.ready @initBoard
+ Main.ready @initReady
- if g.BOARD.ID is 'g'
+ initBoard: ->
+ if g.BOARD.config.code_tags
$.on window, 'prettyprint:cb', (e) ->
return if not (post = g.posts[e.detail.ID])
return if not (pre = $$('.prettyprint', post.nodes.comment)[e.detail.i])
@@ -21,10 +24,12 @@ Fourchan =
}, false);
'''
Callbacks.Post.push
- name: 'Parse /g/ code'
- cb: @code
+ name: 'Parse [code] tags'
+ cb: Fourchan.code
+ g.posts.forEach (post) -> Callbacks.Post.execute post, ['Parse [code] tags'], true
+ ExpandComment.callbacks.push Fourchan.code
- if g.BOARD.ID is 'sci'
+ if g.BOARD.config.math_tags
$.global ->
window.addEventListener 'mathjax', (e) ->
if window.MathJax
@@ -40,16 +45,18 @@ Fourchan =
, false
, false
Callbacks.Post.push
- name: 'Parse /sci/ math'
- cb: @math
+ name: 'Parse [math] tags'
+ cb: Fourchan.math
+ g.posts.forEach (post) -> Callbacks.Post.execute post, ['Parse [math] tags'], true
+ ExpandComment.callbacks.push Fourchan.math
- # Disable 4chan's ID highlighting (replaced by IDHighlight) and reported post hiding.
- Main.ready ->
- $.global ->
- window.clickable_ids = false
- for node in document.querySelectorAll '.posteruid, .capcode'
- node.removeEventListener 'click', window.idClick, false
- return
+ # Disable 4chan's ID highlighting (replaced by IDHighlight) and reported post hiding.
+ initReady: ->
+ $.global ->
+ window.clickable_ids = false
+ for node in document.querySelectorAll '.posteruid, .capcode'
+ node.removeEventListener 'click', window.idClick, false
+ return
code: ->
return if @isClone
diff --git a/src/Miscellaneous/Keybinds.coffee b/src/Miscellaneous/Keybinds.coffee
index eac853916..9609f038c 100644
--- a/src/Miscellaneous/Keybinds.coffee
+++ b/src/Miscellaneous/Keybinds.coffee
@@ -109,6 +109,9 @@ Keybinds =
when Conf['Toggle thread watcher']
return unless ThreadWatcher.enabled
ThreadWatcher.toggleWatcher()
+ when Conf['Toggle threading']
+ return unless QuoteThreading.ready
+ QuoteThreading.toggleThreading()
when Conf['Mark thread read']
return unless g.VIEW is 'index' and thread and UnreadIndex.enabled
UnreadIndex.markRead.call threadRoot
diff --git a/src/Miscellaneous/Report.coffee b/src/Miscellaneous/Report.coffee
index 9ae50029e..5ce9d1746 100644
--- a/src/Miscellaneous/Report.coffee
+++ b/src/Miscellaneous/Report.coffee
@@ -76,14 +76,13 @@ Report =
results = []
for [name, url] in urls
do (name, url) ->
- $.ajax url,
- responseType: 'json'
+ $.ajax url, {
onloadend: ->
results.push [name, @response or {error: ''}]
if results.length is urls.length
cb results
- ,
- {form}
+ form
+ }
return
archiveResults: (results) ->
diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee
index ca210334d..014cd8a67 100644
--- a/src/Monitoring/ThreadStats.coffee
+++ b/src/Monitoring/ThreadStats.coffee
@@ -75,9 +75,11 @@ ThreadStats =
$.addClass ThreadStats.pageCountEl, 'warning'
return
ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
- $.ajax "#{location.protocol}//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
- whenModified: 'ThreadStats'
- bypassCache: true
+ $.whenModified(
+ Site.urls.threadsListJSON({boardID: ThreadStats.thread.board}),
+ 'ThreadStats',
+ ThreadStats.onThreadsLoad
+ )
onThreadsLoad: ->
if @status is 200
diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee
index c44851170..99e16b8d3 100644
--- a/src/Monitoring/ThreadUpdater.coffee
+++ b/src/Monitoring/ThreadUpdater.coffee
@@ -128,17 +128,17 @@ ThreadUpdater =
$.cb.value.call @ if e
load: ->
- {req} = ThreadUpdater
- switch req.status
+ return if @ isnt ThreadUpdater.req # aborted
+ switch @status
when 200
- ThreadUpdater.parse req
+ ThreadUpdater.parse @
if ThreadUpdater.thread.isArchived
ThreadUpdater.kill()
else
ThreadUpdater.setInterval()
when 404
# XXX workaround for 4chan sending false 404s
- $.ajax "#{location.protocol}//a.4cdn.org/#{ThreadUpdater.thread.board}/catalog.json", onloadend: ->
+ $.ajax Site.urls.catalogJSON({boardID: ThreadUpdater.thread.board.ID}), onloadend: ->
if @status is 200
confirmed = true
for page in @response
@@ -151,9 +151,9 @@ ThreadUpdater =
if confirmed
ThreadUpdater.kill()
else
- ThreadUpdater.error req
+ ThreadUpdater.error @
else
- ThreadUpdater.error req
+ ThreadUpdater.error @
kill: ->
ThreadUpdater.thread.kill()
@@ -230,13 +230,15 @@ ThreadUpdater =
update: ->
clearTimeout ThreadUpdater.timeoutID
ThreadUpdater.set 'timer', '...', 'loading'
- ThreadUpdater.req?.abort()
- ThreadUpdater.req = $.ajax "#{location.protocol}//a.4cdn.org/#{ThreadUpdater.thread.board}/thread/#{ThreadUpdater.thread}.json",
- onloadend: ThreadUpdater.cb.load
- timeout: $.MINUTE
- ,
- whenModified: 'ThreadUpdater'
- bypassCache: true
+ if (oldReq = ThreadUpdater.req)
+ delete ThreadUpdater.req
+ oldReq.abort()
+ ThreadUpdater.req = $.whenModified(
+ Site.urls.threadJSON({boardID: ThreadUpdater.thread.board.ID, threadID: ThreadUpdater.thread.ID}),
+ 'ThreadUpdater',
+ ThreadUpdater.cb.load,
+ {timeout: $.MINUTE}
+ )
updateThreadStatus: (type, status) ->
return if not (hasChanged = ThreadUpdater.thread["is#{type}"] isnt status)
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index d63002850..53cdbe12f 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -10,8 +10,8 @@ ThreadWatcher =
className: 'fa fa-eye'
@db = new DataBoard 'watchedThreads', @refresh, true
+ @dbLM = new DataBoard 'watcherLastModified', null, true
@dialog = UI.dialog 'thread-watcher', <%= readHTML('ThreadWatcher.html') %>
-
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
@refreshButton = $ '.refresh', @dialog
@@ -41,6 +41,7 @@ ThreadWatcher =
Header.addShortcut 'watcher', sc, 510
+ ThreadWatcher.initLastModified()
ThreadWatcher.fetchAuto()
$.on window, 'visibilitychange focus', -> $.queueTask ThreadWatcher.fetchAuto
@@ -92,16 +93,16 @@ ThreadWatcher =
href: 'javascript:;'
className: 'watch-thread-link'
$.before $('input', @nodes.info), toggler
+ siteID = Site.hostname
boardID = @board.ID
threadID = @thread.ID
- data = ThreadWatcher.db.get {boardID, threadID}
+ data = ThreadWatcher.db.get {siteID, boardID, threadID}
ThreadWatcher.setToggler toggler, !!data
$.on toggler, 'click', ThreadWatcher.cb.toggle
# Add missing excerpt for threads added by Auto Watch
if data and not data.excerpt?
$.queueTask =>
- ThreadWatcher.db.extend {boardID, threadID, val: {excerpt: Get.threadExcerpt @thread}}
- ThreadWatcher.refresh()
+ ThreadWatcher.update siteID, boardID, threadID, val: {excerpt: Get.threadExcerpt @thread}
catalogNode: ->
$.addClass @nodes.root, 'watched' if ThreadWatcher.isWatched @thread
@@ -153,13 +154,14 @@ ThreadWatcher =
for threadID, data of db.data[siteID].boards[boardID] when not data?.isDead and "#{boardID}.#{threadID}" not in e.detail.threads
# Don't prune threads that have yet to appear in index.
continue unless e.detail.threads.some (fullID) -> +fullID.split('.')[1] > threadID
- nKilled++
if Conf['Auto Prune'] or not (data and typeof data is 'object') # corrupt data
db.delete {boardID, threadID}
+ nKilled++
+ else if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
+ ThreadWatcher.fetchStatus {siteID, boardID, threadID, data}
else
- db.extend {boardID, threadID, val: {isDead: true}}
- if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
- ThreadWatcher.fetchStatus {siteID, boardID, threadID, data}
+ db.extend {boardID, threadID, val: {isDead: true, page: undefined, lastPage: undefined, unread: undefined, quotingYou: undefined}}
+ nKilled++
ThreadWatcher.refresh() if nKilled
onThreadRefresh: (e) ->
thread = g.threads[e.detail.threadID]
@@ -170,6 +172,30 @@ ThreadWatcher =
requests: []
fetched: 0
+ fetch: (url, {siteID, force}, args, cb) ->
+ if ThreadWatcher.requests.length is 0
+ ThreadWatcher.status.textContent = '...'
+ $.addClass ThreadWatcher.refreshButton, 'fa-spin'
+ onloadend = ->
+ return if @finished
+ @finished = true
+ ThreadWatcher.fetched++
+ if ThreadWatcher.fetched is ThreadWatcher.requests.length
+ ThreadWatcher.clearRequests()
+ else
+ ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
+ cb.apply @, args
+ ajax = if siteID is Site.hostname then $.ajax else CrossOrigin.ajax
+ if force
+ delete $.lastModified.ThreadWatcher?[url]
+ req = $.whenModified(
+ url,
+ 'ThreadWatcher',
+ onloadend,
+ {timeout: $.MINUTE, ajax}
+ )
+ ThreadWatcher.requests.push req
+
clearRequests: ->
ThreadWatcher.requests = []
ThreadWatcher.fetched = 0
@@ -177,78 +203,137 @@ ThreadWatcher =
$.rmClass ThreadWatcher.refreshButton, 'fa-spin'
abort: ->
- for req in ThreadWatcher.requests when req.readyState isnt 4 # DONE
+ delete ThreadWatcher.syncing
+ for req in ThreadWatcher.requests when !req.finished
+ req.finished = true
req.abort()
ThreadWatcher.clearRequests()
+ initLastModified: ->
+ lm = ($.lastModified['ThreadWatcher'] or= {})
+ for siteID, boards of ThreadWatcher.dbLM.data
+ for boardID, data of boards.boards
+ if ThreadWatcher.db.get {siteID, boardID}
+ for url, date of data
+ lm[url] = date
+ else
+ ThreadWatcher.dbLM.delete {siteID, boardID}
+ return
+
fetchAuto: ->
clearTimeout ThreadWatcher.timeout
return unless Conf['Auto Update Thread Watcher']
{db} = ThreadWatcher
- interval = if ThreadWatcher.unreadEnabled and Conf['Show Unread Count'] then 5 * $.MINUTE else 2 * $.HOUR
+ interval = if Conf['Show Page'] or (ThreadWatcher.unreadEnabled and Conf['Show Unread Count']) then 5 * $.MINUTE else 2 * $.HOUR
now = Date.now()
unless now - interval < (db.data.lastChecked or 0) <= now or d.hidden or not d.hasFocus()
ThreadWatcher.fetchAllStatus()
- db.setLastChecked()
ThreadWatcher.timeout = setTimeout ThreadWatcher.fetchAuto, interval
buttonFetchAll: ->
- if ThreadWatcher.requests.length
+ if ThreadWatcher.syncing or ThreadWatcher.requests.length
ThreadWatcher.abort()
else
ThreadWatcher.fetchAllStatus()
fetchAllStatus: ->
+ ThreadWatcher.status.textContent = '...'
+ $.addClass ThreadWatcher.refreshButton, 'fa-spin'
+ ThreadWatcher.syncing = true
dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter((x) -> x)
n = 0
- for db in dbs
- db.forceSync ->
+ for dbi in dbs
+ dbi.forceSync ->
if (++n) is dbs.length
- threads = ThreadWatcher.getAll()
- for thread in threads
- ThreadWatcher.fetchStatus thread
- return
+ return if !ThreadWatcher.syncing # aborted
+ delete ThreadWatcher.syncing
+ # XXX On vichan boards, last_modified field of threads.json does not account for sage posts.
+ # Occasionally check replies field of catalog.json to find these posts.
+ {db} = ThreadWatcher
+ now = Date.now()
+ deep = !(now - 2 * $.HOUR < (db.data.lastChecked2 or 0) <= now)
+ boards = ThreadWatcher.getAll(true)
+ for board in boards
+ ThreadWatcher.fetchBoard board, deep
+ db.setLastChecked()
+ db.setLastChecked('lastChecked2') if deep
+ if ThreadWatcher.fetched is ThreadWatcher.requests.length
+ ThreadWatcher.clearRequests()
- fetchStatus: (thread, force) ->
- {siteID, boardID, threadID, data} = thread
+ fetchBoard: (board, deep) ->
+ return unless board.some (thread) -> !thread.data.isDead
+ force = Conf['Show Page'] and board.some((thread) -> !thread.data.page? and !thread.data.isDead and thread.data.last isnt -1)
+ {siteID, boardID} = board[0]
+ software = Conf['siteProperties'][siteID]?.software
+ urlF = if deep and software is 'tinyboard' then 'catalogJSON' else 'threadsListJSON'
+ url = SW[software]?.urls[urlF]?({siteID, boardID})
+ return unless url
+ ThreadWatcher.fetch url, {siteID, force}, [board, url], ThreadWatcher.parseBoard
+
+ parseBoard: (board, url) ->
+ return unless @status is 200
+ {siteID, boardID} = board[0]
+ software = Conf['siteProperties'][siteID]?.software
+ lmDate = @getResponseHeader('Last-Modified')
+ ThreadWatcher.dbLM.extend {siteID, boardID, val: $.item(url, lmDate)}
+ threads = {}
+ pageLength = 0
+ nThreads = 0
+ oldest = null
+ try
+ pageLength = @response[0]?.threads.length or 0
+ for page, i in @response
+ for item in page.threads
+ threads[item.no] =
+ page: i + 1
+ index: nThreads
+ modified: item.last_modified
+ replies: item.replies
+ nThreads++
+ if !oldest? or item.no < oldest
+ oldest = item.no
+ catch
+ for thread in board
+ ThreadWatcher.fetchStatus thread
+ for thread in board
+ {threadID, data} = thread
+ if threads[threadID]
+ {page, index, modified, replies} = threads[threadID]
+ if Conf['Show Page']
+ lastPage = if SW[software]?.isPrunedByAge?({siteID, boardID})
+ threadID is oldest
+ else
+ index >= nThreads - pageLength
+ ThreadWatcher.update siteID, boardID, threadID, {page, lastPage}
+ if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
+ if modified isnt data.modified or (replies? and replies isnt data.replies)
+ ThreadWatcher.db.extend {siteID, boardID, threadID, val: {modified}}
+ ThreadWatcher.fetchStatus thread
+ else
+ if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
+ ThreadWatcher.fetchStatus thread
+ else
+ ThreadWatcher.update siteID, boardID, threadID, {isDead: true}
+ return
+
+ fetchStatus: (thread) ->
+ {siteID, boardID, threadID, data, force} = thread
software = Conf['siteProperties'][siteID]?.software
url = SW[software]?.urls.threadJSON?({siteID, boardID, threadID})
return unless url
return if data.isDead and not force
return if data.last is -1 # 404 or no JSON API
- if ThreadWatcher.requests.length is 0
- ThreadWatcher.status.textContent = '...'
- $.addClass ThreadWatcher.refreshButton, 'fa-spin'
- if Site.hasCORS?(url) or url.split('/')[...3].join('/') is location.origin
- req = $.ajax url,
- onloadend: ->
- ThreadWatcher.parseStatus.call @, thread
- timeout: $.MINUTE
- ,
- whenModified: if force then false else 'ThreadWatcher'
- else
- req = {abort: () -> req.aborted = true}
- CrossOrigin.json url, ->
- return if req.aborted
- ThreadWatcher.parseStatus.call @, thread
- , true, $.MINUTE
- ThreadWatcher.requests.push req
+ ThreadWatcher.fetch url, {siteID, force}, [thread], ThreadWatcher.parseStatus
parseStatus: ({siteID, boardID, threadID, data}) ->
- ThreadWatcher.fetched++
- if ThreadWatcher.fetched is ThreadWatcher.requests.length
- ThreadWatcher.clearRequests()
- else
- ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
-
software = Conf['siteProperties'][siteID]?.software
if @status is 200 and @response
last = @response.posts[@response.posts.length-1].no
+ replies = @response.posts.length-1
isDead = !!@response.posts[0].archived
if isDead and Conf['Auto Prune']
- ThreadWatcher.db.delete {siteID, boardID, threadID}
- ThreadWatcher.refresh()
+ ThreadWatcher.rm siteID, boardID, threadID
return
return if last is data.last and isDead is data.isDead
@@ -264,7 +349,7 @@ ThreadWatcher =
unread++
- if !quotingYou and !Conf['Require OP Quote Link'] and youOP and not Filter.isHidden(Build.parseJSON postObj, boardID)
+ if !quotingYou and !Conf['Require OP Quote Link'] and youOP and not Filter.isHidden(Build.parseJSON postObj, boardID, siteID)
quotingYou = true
continue
@@ -282,31 +367,27 @@ ThreadWatcher =
}
quotesYou = true
break
- if quotesYou and not Filter.isHidden(Build.parseJSON postObj, boardID)
+ if quotesYou and not Filter.isHidden(Build.parseJSON postObj, boardID, siteID)
quotingYou = true
- updated = (isDead isnt data.isDead or unread isnt data.unread or quotingYou isnt data.quotingYou)
- ThreadWatcher.db.extend {siteID, boardID, threadID, val: {last, isDead, unread, quotingYou}}
- ThreadWatcher.refresh() if updated
+ ThreadWatcher.update siteID, boardID, threadID, {last, replies, isDead, unread, quotingYou}
else if @status is 404
if SW[software].mayLackJSON and !data.last?
- ThreadWatcher.db.extend {siteID, boardID, threadID, val: {last: -1}, rm: ['unread', 'quotingYou']}
- else if Conf['Auto Prune']
- ThreadWatcher.db.delete {siteID, boardID, threadID}
+ ThreadWatcher.update siteID, boardID, threadID, {last: -1}
else
- ThreadWatcher.db.extend {siteID, boardID, threadID, val: {isDead: true}, rm: ['unread', 'quotingYou']}
+ ThreadWatcher.update siteID, boardID, threadID, {isDead: true}
- ThreadWatcher.refresh()
-
- getAll: ->
+ getAll: (groupByBoard) ->
all = []
for siteID, boards of ThreadWatcher.db.data
for boardID, threads of boards.boards
if Conf['Current Board'] and (siteID isnt Site.hostname or boardID isnt g.BOARD.ID)
continue
+ if groupByBoard
+ all.push (cont = [])
for threadID, data of threads when data and typeof data is 'object'
- all.push {siteID, boardID, threadID, data}
+ (if groupByBoard then cont else all).push {siteID, boardID, threadID, data}
all
makeLine: (siteID, boardID, threadID, data) ->
@@ -326,6 +407,12 @@ ThreadWatcher =
title: excerpt
className: 'watcher-link'
+ if Conf['Show Page'] and data.page?
+ page = $.el 'span',
+ textContent: "[#{data.page}]"
+ className: 'watcher-page'
+ $.add link, page
+
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count'] and data.unread?
count = $.el 'span',
textContent: "(#{data.unread})"
@@ -343,6 +430,9 @@ ThreadWatcher =
div.dataset.siteID = siteID
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
+ if Conf['Show Page']
+ $.addClass div, 'last-page' if data.lastPage
+ div.dataset.page = data.page if data.page?
if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
$.addClass div, 'replies-read' if data.unread is 0
$.addClass div, 'replies-unread' if data.unread
@@ -384,9 +474,6 @@ ThreadWatcher =
$.add list, nodes
ThreadWatcher.refreshIcon()
- for refresher in ThreadWatcher.menu.refreshers
- refresher()
- return
refresh: ->
ThreadWatcher.build()
@@ -407,19 +494,19 @@ ThreadWatcher =
ThreadWatcher.shortcut.classList.toggle className, !!$(".#{className}", ThreadWatcher.dialog)
return
- update: (boardID, threadID, newData) ->
- siteID = Site.hostname
- return if not (data = ThreadWatcher.db?.get {boardID, threadID})
+ update: (siteID, boardID, threadID, newData) ->
+ return if not (data = ThreadWatcher.db?.get {siteID, boardID, threadID})
if newData.isDead and Conf['Auto Prune']
- ThreadWatcher.db.delete {boardID, threadID}
- ThreadWatcher.refresh()
+ ThreadWatcher.rm siteID, boardID, threadID
return
+ if newData.isDead or newData.last is -1
+ for key in ['page', 'lastPage', 'unread', 'quotingyou'] when key not of newData
+ newData[key] = undefined
n = 0
n++ for key, val of newData when data[key] isnt val
return unless n
- return if not (data = ThreadWatcher.db.get {boardID, threadID})
- ThreadWatcher.db.extend {boardID, threadID, val: newData}
- if line = $ "#watched-threads > [data-site-i-d='#{siteID}'][data-full-i-d='#{boardID}.#{threadID}']", ThreadWatcher.dialog
+ ThreadWatcher.db.extend {siteID, boardID, threadID, val: newData}
+ if (line = $ "#watched-threads > [data-site-i-d='#{siteID}'][data-full-i-d='#{boardID}.#{threadID}']", ThreadWatcher.dialog)
newLine = ThreadWatcher.makeLine siteID, boardID, threadID, data
$.replace line, newLine
ThreadWatcher.refreshIcon()
@@ -431,8 +518,8 @@ ThreadWatcher =
if Conf['Auto Prune']
ThreadWatcher.db.delete {boardID, threadID}
return cb()
- return cb() if data.isDead and not (data.unread? or data.quotingYou?)
- ThreadWatcher.db.extend {boardID, threadID, val: {isDead: true}, rm: ['unread', 'quotingYou']}, cb
+ return cb() if data.isDead and not (data.page? or data.lastPage? or data.unread? or data.quotingYou?)
+ ThreadWatcher.db.extend {boardID, threadID, val: {isDead: true, page: undefined, lastPage: undefined, unread: undefined, quotingYou: undefined}}, cb
toggle: (thread) ->
siteID = Site.hostname
@@ -459,15 +546,17 @@ ThreadWatcher =
addRaw: (boardID, threadID, data) ->
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
- if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
- ThreadWatcher.fetchStatus {siteID: Site.hostname, boardID, threadID, data}, true
+ thread = {siteID: Site.hostname, boardID, threadID, data, force: true}
+ if Conf['Show Page'] and !data.isDead
+ ThreadWatcher.fetchBoard [thread]
+ else if ThreadWatcher.unreadEnabled and Conf['Show Unread Count']
+ ThreadWatcher.fetchStatus thread
rm: (siteID, boardID, threadID) ->
ThreadWatcher.db.delete {siteID, boardID, threadID}
ThreadWatcher.refresh()
menu:
- refreshers: []
init: ->
return if !Conf['Thread Watcher']
menu = @menu = new UI.Menu 'thread watcher'
@@ -482,53 +571,52 @@ ThreadWatcher =
Header.menu.addEntry
el: entryEl
order: 60
+ open: ->
+ [addClass, rmClass, text] = if !!ThreadWatcher.db.get {boardID: g.BOARD.ID, threadID: g.THREADID}
+ ['unwatch-thread', 'watch-thread', 'Unwatch thread']
+ else
+ ['watch-thread', 'unwatch-thread', 'Watch thread']
+ $.addClass entryEl, addClass
+ $.rmClass entryEl, rmClass
+ entryEl.textContent = text
+ true
$.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
- @refreshers.push ->
- [addClass, rmClass, text] = if $ '.current', ThreadWatcher.list
- ['unwatch-thread', 'watch-thread', 'Unwatch thread']
- else
- ['watch-thread', 'unwatch-thread', 'Watch thread']
- $.addClass entryEl, addClass
- $.rmClass entryEl, rmClass
- entryEl.textContent = text
addMenuEntries: ->
entries = []
# `Open all` entry
entries.push
+ text: 'Open all threads'
cb: ThreadWatcher.cb.openAll
- entry:
- el: $.el 'a',
- textContent: 'Open all threads'
- refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
+ open: ->
+ @el.classList.toggle 'disabled', !ThreadWatcher.list.firstElementChild
+ true
# `Prune dead threads` entry
entries.push
+ text: 'Prune dead threads'
cb: ThreadWatcher.cb.pruneDeads
- entry:
+ open: ->
+ @el.classList.toggle 'disabled', !$('.dead-thread', ThreadWatcher.list)
+ true
+
+ for {text, cb, open} in entries
+ entry =
el: $.el 'a',
- textContent: 'Prune dead threads'
- refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
-
- # `Settings` entries:
- subEntries = []
- for name, conf of Config.threadWatcher
- subEntries.push @createSubEntry name, conf[1]
- entries.push
- entry:
- el: $.el 'span',
- textContent: 'Settings'
- subEntries: subEntries
-
- for {entry, cb, refresh} in entries
- entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
- $.on entry.el, 'click', cb if cb
- @refreshers.push refresh.bind entry if refresh
+ textContent: text
+ href: 'javascript:;'
+ $.on entry.el, 'click', cb
+ entry.open = open.bind(entry)
@menu.addEntry entry
+
+ # Settings checkbox entries:
+ for name, conf of Config.threadWatcher
+ @addCheckbox name, conf[1]
+
return
- createSubEntry: (name, desc) ->
+ addCheckbox: (name, desc) ->
entry =
type: 'thread watcher'
el: UI.checkbox name, name.replace(' Thread Watcher', '')
@@ -539,6 +627,6 @@ ThreadWatcher =
$.addClass entry.el, 'disabled'
entry.el.title += '\n[Remember Last Read Post is disabled.]'
$.on input, 'change', $.cb.checked
- $.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Unread Count', 'Show Site Prefix']
- $.on input, 'change', ThreadWatcher.fetchAuto if name in ['Show Unread Count', 'Auto Update Thread Watcher']
- entry
+ $.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Page', 'Show Unread Count', 'Show Site Prefix']
+ $.on input, 'change', ThreadWatcher.fetchAuto if name in ['Show Page', 'Show Unread Count', 'Auto Update Thread Watcher']
+ @menu.addEntry entry
diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee
index 3e2aa740d..2f39d0775 100644
--- a/src/Monitoring/Unread.coffee
+++ b/src/Monitoring/Unread.coffee
@@ -125,9 +125,9 @@ Unread =
Unread.openNotification post
return
- openNotification: (post) ->
+ openNotification: (post, predicate=' replied to you') ->
return unless Header.areNotificationsEnabled
- notif = new Notification "#{post.info.nameBlock} replied to you",
+ notif = new Notification "#{post.info.nameBlock}#{predicate}",
body: post.commentDisplay()
icon: Favicon.logo
notif.onclick = ->
@@ -238,7 +238,7 @@ Unread =
saveThreadWatcherCount: $.debounce 2 * $.SECOND, ->
$.forceSync 'Remember Last Read Post'
if Conf['Remember Last Read Post'] and (!Unread.thread.isDead or Unread.thread.isArchived)
- ThreadWatcher.update Unread.thread.board.ID, Unread.thread.ID,
+ ThreadWatcher.update Site.hostname, Unread.thread.board.ID, Unread.thread.ID,
isDead: Unread.thread.isDead
unread: Unread.posts.size
quotingYou: !!(if !Conf['Require OP Quote Link'] and QuoteYou.isYou(Unread.thread.OP) then Unread.posts.size else Unread.postsQuotingYou.size)
diff --git a/src/Monitoring/UnreadIndex.coffee b/src/Monitoring/UnreadIndex.coffee
index 46606aef8..4d2878352 100644
--- a/src/Monitoring/UnreadIndex.coffee
+++ b/src/Monitoring/UnreadIndex.coffee
@@ -94,13 +94,10 @@ UnreadIndex =
markRead: ->
thread = Get.threadFromNode @
- if Index.enabled
- lastPost = Index.lastPost(thread.ID)
- else
- lastPost = 0
- thread.posts.forEach (post) ->
- if post.ID > lastPost and !post.isFetchedQuote
- lastPost = post.ID
+ lastPost = if Index.enabled then Index.lastPost(thread.ID) else 0
+ thread.posts.forEach (post) ->
+ if post.ID > lastPost and !post.isFetchedQuote
+ lastPost = post.ID
UnreadIndex.lastReadPost[thread.fullID] = lastPost
UnreadIndex.db.set
boardID: thread.board.ID
@@ -108,6 +105,6 @@ UnreadIndex =
val: lastPost
$.rm UnreadIndex.hr[thread.fullID]
thread.nodes.root.classList.remove 'unread-thread'
- ThreadWatcher.update thread.board.ID, thread.ID,
+ ThreadWatcher.update Site.hostname, thread.board.ID, thread.ID,
unread: 0
quotingYou: false
diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee
index 4412dd876..20a340de8 100644
--- a/src/Posting/QR.coffee
+++ b/src/Posting/QR.coffee
@@ -707,41 +707,29 @@ QR =
options =
responseType: 'document'
withCredentials: true
- onload: QR.response
- onerror: ->
- # On connection error, the post most likely didn't go through.
- # If the post did go through, it should be stopped by the duplicate reply cooldown.
- delete QR.req
- Captcha.cache.save QR.currentCaptcha if QR.currentCaptcha
- delete QR.currentCaptcha
- post.unlock()
- QR.cooldown.auto = true
- QR.cooldown.addDelay post, 2
- QR.status()
- QR.error QR.connectionError()
- extra =
+ onloadend: QR.response
form: $.formData formData
if Conf['Show Upload Progress']
- extra.upCallbacks =
- onload: ->
+ options.onprogress = (e) ->
+ return if @ isnt QR.req?.upload # aborted
+ if e.loaded < e.total
+ # Uploading...
+ QR.req.progress = "#{Math.round e.loaded / e.total * 100}%"
+ else
# 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()
+ QR.status()
cb = (response) ->
if response?
QR.currentCaptcha = response
if response.challenge?
- extra.form.append 'recaptcha_challenge_field', response.challenge
- extra.form.append 'recaptcha_response_field', response.response
+ options.form.append 'recaptcha_challenge_field', response.challenge
+ options.form.append 'recaptcha_response_field', response.response
else
- extra.form.append 'g-recaptcha-response', response.response
- QR.req = $.ajax "https://sys.#{location.hostname.split('.')[1]}.org/#{g.BOARD}/post", options, extra
+ options.form.append 'g-recaptcha-response', response.response
+ QR.req = $.ajax "https://sys.#{location.hostname.split('.')[1]}.org/#{g.BOARD}/post", options
QR.req.progress = '...'
if typeof captcha is 'function'
@@ -765,20 +753,19 @@ QR =
QR.status()
response: ->
- {req} = QR
+ return if @ isnt QR.req # aborted
delete QR.req
post = QR.posts[0]
post.unlock()
- resDoc = req.response
- if (err = resDoc.getElementById 'errmsg') # error!
+ if (err = @response?.getElementById 'errmsg') # error!
$('a', err)?.target = '_blank' # duplicate image link
- else if (connErr = resDoc.title isnt 'Post successful!')
+ else if (connErr = (!@response or @response.title isnt 'Post successful!'))
err = QR.connectionError()
Captcha.cache.save QR.currentCaptcha if QR.currentCaptcha
- else if req.status isnt 200
- err = "Error #{req.statusText} (#{req.status})"
+ else if @status isnt 200
+ err = "Error #{@statusText} (#{@status})"
delete QR.currentCaptcha
@@ -810,7 +797,7 @@ QR =
QR.error err
return
- h1 = $ 'h1', resDoc
+ h1 = $ 'h1', @response
[_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
postID = +postID
@@ -880,14 +867,14 @@ QR =
cb()
else
setTimeout check, attempts * $.SECOND
- ,
+ responseType: 'text'
type: 'HEAD'
check()
abort: ->
- if QR.req and !QR.req.isUploadFinished
- QR.req.abort()
+ if (oldReq = QR.req) and !QR.req.isUploadFinished
delete QR.req
+ oldReq.abort()
Captcha.cache.save QR.currentCaptcha if QR.currentCaptcha
delete QR.currentCaptcha
QR.posts[0].unlock()
diff --git a/src/Quotelinks/QuoteThreading.coffee b/src/Quotelinks/QuoteThreading.coffee
index 4c28cefe6..91fbfc258 100644
--- a/src/Quotelinks/QuoteThreading.coffee
+++ b/src/Quotelinks/QuoteThreading.coffee
@@ -38,6 +38,14 @@ QuoteThreading =
children: {}
inserted: {}
+ toggleThreading: ->
+ @setThreadingState !Conf['Thread Quotes']
+
+ setThreadingState: (enabled) ->
+ @input.checked = enabled
+ @setEnabled.call @input
+ @rethread.call @input
+
setEnabled: ->
if @checked
$.set 'Prune All Threads', false
diff --git a/src/classes/Callbacks.coffee b/src/classes/Callbacks.coffee
index 797bd6129..f35629d9b 100644
--- a/src/classes/Callbacks.coffee
+++ b/src/classes/Callbacks.coffee
@@ -10,8 +10,8 @@ class Callbacks
@keys.push name unless @[name]
@[name] = cb
- execute: (node, keys=@keys) ->
- return if node.callbacksExecuted
+ execute: (node, keys=@keys, force) ->
+ return if node.callbacksExecuted and !force
node.callbacksExecuted = true
for name in keys
try
diff --git a/src/classes/DataBoard.coffee b/src/classes/DataBoard.coffee
index 3a07e3d4c..ddb7c621f 100644
--- a/src/classes/DataBoard.coffee
+++ b/src/classes/DataBoard.coffee
@@ -1,5 +1,5 @@
class DataBoard
- @keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']
+ @keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']
constructor: (@key, sync, dontClean) ->
@initData Conf[@key]
@@ -85,17 +85,20 @@ class DataBoard
else
@data[siteID].boards[boardID] = val
- extend: ({siteID, boardID, threadID, postID, val, rm}, cb) ->
+ extend: ({siteID, boardID, threadID, postID, val}, cb) ->
@save =>
- oldVal = @get {siteID, boardID, threadID, postID, val: {}}
- delete oldVal[key] for key in rm or []
- $.extend oldVal, val
+ oldVal = @get {siteID, boardID, threadID, postID, defaultValue: {}}
+ for key, subVal of val
+ if typeof subVal is 'undefined'
+ delete oldVal[key]
+ else
+ oldVal[key] = subVal
@setUnsafe {siteID, boardID, threadID, postID, val: oldVal}
, cb
- setLastChecked: ->
+ setLastChecked: (key='lastChecked') ->
@save =>
- @data.lastChecked = Date.now()
+ @data[key] = Date.now()
get: ({siteID, boardID, threadID, postID, defaultValue}) ->
siteID or= Site.hostname
@@ -116,13 +119,9 @@ class DataBoard
val or defaultValue
clean: ->
- # XXX not yet multisite ready
- return unless Site.software is 'yotsuba'
siteID = Site.hostname
-
for boardID, val of @data[siteID].boards
@deleteIfEmpty {siteID, boardID}
-
now = Date.now()
unless now - 2 * $.HOUR < (@data[siteID].lastChecked or 0) <= now
@data[siteID].lastChecked = now
@@ -131,12 +130,18 @@ class DataBoard
return
ajaxClean: (boardID) ->
- $.cache "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json", (e1) =>
- return unless e1.target.status is 200
- response1 = e1.target.response
- $.cache "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json", (e2) =>
- return unless e2.target.status is 200 or boardID in ['b', 'f', 'trash', 'bant']
- @ajaxCleanParse boardID, response1, e2.target.response
+ that = @
+ siteID = Site.hostname
+ threadsList = Site.urls.threadsListJSON?({siteID, boardID})
+ return unless threadsList
+ $.cache threadsList, ->
+ return unless @status is 200
+ archiveList = Site.urls.archiveListJSON?({siteID, boardID})
+ return that.ajaxCleanParse(boardID, @response) unless archiveList
+ response1 = @response
+ $.cache archiveList, ->
+ return unless @status is 200
+ that.ajaxCleanParse(boardID, response1, @response)
ajaxCleanParse: (boardID, response1, response2) ->
siteID = Site.hostname
diff --git a/src/classes/Fetcher.coffee b/src/classes/Fetcher.coffee
index 1e144c68b..693fb2856 100644
--- a/src/classes/Fetcher.coffee
+++ b/src/classes/Fetcher.coffee
@@ -15,8 +15,9 @@ class Fetcher
@root.textContent = "Loading post No.#{@postID}..."
if @threadID
- $.cache "#{location.protocol}//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json", (e, isCached) =>
- @fetchedPost e.target, isCached
+ that = @
+ $.cache Site.urls.threadJSON({boardID: @boardID, threadID: @threadID}), ({isCached}) ->
+ that.fetchedPost @, isCached
else
@archivedPost()
@@ -60,12 +61,14 @@ class Fetcher
{status} = req
unless status is 200
# The thread can die by the time we check a quote.
- return if @archivedPost()
+ return if status and @archivedPost()
$.addClass @root, 'warning'
@root.textContent =
if status is 404
"Thread No.#{@threadID} 404'd."
+ else if !status
+ 'Connection Error'
else
"Error #{req.statusText} (#{req.status})."
return
@@ -78,10 +81,11 @@ class Fetcher
if post.no isnt @postID
# Cached requests can be stale and must be rechecked.
if isCached
- api = "#{location.protocol}//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json"
+ api = Site.urls.threadJSON({boardID: @boardID, threadID: @threadID})
$.cleanCache (url) -> url is api
- $.cache api, (e) =>
- @fetchedPost e.target, false
+ that = @
+ $.cache api, ->
+ that.fetchedPost @, false
return
# The post can be deleted by the time we check a quote.
@@ -107,7 +111,7 @@ class Fetcher
encryptionOK = /^https:\/\//.test(url) or location.protocol is 'http:'
if encryptionOK or Conf['Exempt Archives from Encryption']
that = @
- CrossOrigin.json url, ->
+ CrossOrigin.cache url, ->
if !encryptionOK and @response?.media
{media} = @response
for key of media when /_link$/.test key
diff --git a/src/classes/Post.coffee b/src/classes/Post.coffee
index 3db8b7ee7..a913a5829 100644
--- a/src/classes/Post.coffee
+++ b/src/classes/Post.coffee
@@ -9,6 +9,7 @@ class Post
@ID = +root.id.match(/\d*$/)[0]
@threadID = @thread.ID
@boardID = @board.ID
+ @siteID = Site.hostname
@fullID = "#{@board}.#{@ID}"
@context = @
@isReply = (@ID isnt @threadID)
@@ -28,6 +29,7 @@ class Post
@info =
subject: @nodes.subject?.textContent or undefined
name: @nodes.name?.textContent
+ email: if @nodes.email then decodeURIComponent(@nodes.email.href.replace(/^mailto:/, ''))
tripcode: @nodes.tripcode?.textContent
uniqueID: @nodes.uniqueID?.textContent
capcode: @nodes.capcode?.textContent.replace '## ', ''
diff --git a/src/config/Config.coffee b/src/config/Config.coffee
index fdcc39acc..f4ddc3dae 100644
--- a/src/config/Config.coffee
+++ b/src/config/Config.coffee
@@ -19,6 +19,10 @@ Config =
'Show a notice at the top of the page when the index is refreshed.'
1
]
+ 'Follow Cursor': [
+ true
+ 'Image Hover and Quote Preview move with the mouse cursor.'
+ ]
'Open Threads in New Tab': [
false
'Make links to threads in the index / <%= meta.name %> catalog open in a new tab.'
@@ -628,7 +632,7 @@ Config =
false
'Advance to next post when contracting an expanded image.'
]
-
+
gallery:
'Hide Thumbnails': [
false
@@ -672,6 +676,10 @@ Config =
false
'Automatically remove dead threads.'
]
+ 'Show Page': [
+ true
+ 'Show what page watched threads are on.'
+ ]
'Show Unread Count': [
true
'Show number of unread posts in watched threads.'
@@ -720,6 +728,8 @@ Config =
#/./
"""
+ email: ''
+
subject: """
# Filter Generals on /v/:
#/general/i;boards:v;op:only
@@ -748,7 +758,7 @@ Config =
sauces: """
# Known filename formats:
http://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/
- //%$1.deviantart.com/gallery/#/d%$2;regexp:/^\\w+_by_(\\w+)-d([\\da-z]+)/
+ //www.deviantart.com/gallery/#/d%$1%$2;regexp:/^\\w+_by_\\w+[_-]d([\\da-z]{6})\\b|^d([\\da-z]{6})-[\\da-z]{8}-/
//imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/
http://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/
https://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/
@@ -955,6 +965,10 @@ Config =
't'
'Toggle visibility of thread watcher.'
]
+ 'Toggle threading': [
+ 'Shift+t'
+ 'Toggle threading.'
+ ]
'Mark thread read': [
'Ctrl+0'
'Mark thread read from index (requires "Unread Line in Index").'
diff --git a/src/css/burichan.css b/src/css/burichan.css
index 70a6ba331..215768fb7 100644
--- a/src/css/burichan.css
+++ b/src/css/burichan.css
@@ -93,7 +93,7 @@
}
/* Thread Watcher */
-:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you {
+:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you, :root.burichan .last-page > a > .watcher-page {
color: #F00;
}
diff --git a/src/css/futaba.css b/src/css/futaba.css
index e8e6bbed9..0203ddc80 100644
--- a/src/css/futaba.css
+++ b/src/css/futaba.css
@@ -93,7 +93,7 @@
}
/* Thread Watcher */
-:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you {
+:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you, :root.futaba .last-page > a > .watcher-page {
color: #F00;
}
diff --git a/src/css/photon.css b/src/css/photon.css
index db18e601d..9fe073b5c 100644
--- a/src/css/photon.css
+++ b/src/css/photon.css
@@ -91,7 +91,7 @@
}
/* Thread Watcher */
-:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you {
+:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you, :root.photon .last-page > a > .watcher-page {
color: #00F !important;
}
diff --git a/src/css/spooky.css b/src/css/spooky.css
index 95dd9c792..9be3345fe 100644
--- a/src/css/spooky.css
+++ b/src/css/spooky.css
@@ -157,7 +157,7 @@
}
/* Thread Watcher */
-:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you {
+:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you, :root.spooky .last-page > a > .watcher-page {
color: #F00 !important;
}
diff --git a/src/css/style.css b/src/css/style.css
index 935f73bdd..9c813ca58 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -129,6 +129,10 @@ body.is_catalog .thread > a > img {
.nwsb {
display: inline;
}
+.fileText {
+ max-width: auto;
+ white-space: normal;
+}
/* Ads */
.ad-cnt > *, .adg-rects > *, .bsa-cnt {
@@ -1164,12 +1168,11 @@ span.hide-announcement {
-webkit-flex-direction: row;
flex-direction: row;
}
+#watched-threads .watcher-page,
#watched-threads .watcher-unread {
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
-}
-#watched-threads .watcher-unread::after {
- content: "\00a0";
+ margin-right: 2px;
}
#watched-threads .watcher-title {
overflow: hidden;
@@ -1177,7 +1180,10 @@ span.hide-announcement {
-webkit-flex: 0 1 auto;
flex: 0 1 auto;
}
-.replies-quoting-you > a, #watcher-link.replies-quoting-you {
+#watched-threads .watcher-title:not(:first-child) {
+ margin-left: 2px;
+}
+.replies-quoting-you > a, #watcher-link.replies-quoting-you, .last-page > a > .watcher-page {
color: #F00;
}
#thread-watcher a {
@@ -1344,6 +1350,13 @@ span.hide-announcement {
.fileThumb > .warning {
clear: both;
}
+#ihover {
+ pointer-events: none;
+ /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
+ max-height: 95vh;
+ max-height: calc(100vh - 25px);
+ max-width: 100vw;
+}
/* WEBM Metadata */
.webm-title > a::before {
content: "title";
diff --git a/src/css/tomorrow.css b/src/css/tomorrow.css
index 0d41b6fbb..26a7bef68 100644
--- a/src/css/tomorrow.css
+++ b/src/css/tomorrow.css
@@ -162,7 +162,7 @@
}
/* Thread Watcher */
-:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you {
+:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you, :root.tomorrow .last-page > a > .watcher-page {
color: #F00 !important;
}
diff --git a/src/css/yotsuba.css b/src/css/yotsuba.css
index cf138a6e8..7ed23a580 100644
--- a/src/css/yotsuba.css
+++ b/src/css/yotsuba.css
@@ -88,7 +88,7 @@
}
/* Thread Watcher */
-:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you {
+:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you, :root.yotsuba .last-page > a > .watcher-page {
color: #F00;
}
diff --git a/src/main/Main.coffee b/src/main/Main.coffee
index 0e6d77925..c767fb62d 100644
--- a/src/main/Main.coffee
+++ b/src/main/Main.coffee
@@ -1,8 +1,5 @@
Main =
init: ->
- # XXX Work around Pale Moon / old Firefox + GM 1.15 bug where script runs in iframe with wrong window.location.
- return if d.body and not $ 'title', d.head
-
# XXX dwb userscripts extension reloads scripts run at document-start when replaceState/pushState is called.
# XXX Firefox reinjects WebExtension content scripts when extension is updated / reloaded.
try
diff --git a/src/meta/eventPage.coffee b/src/meta/eventPage.coffee
index 1b113aa72..8a849b7fd 100644
--- a/src/meta/eventPage.coffee
+++ b/src/meta/eventPage.coffee
@@ -25,16 +25,14 @@ handlers =
xhr.open 'GET', request.url, true
xhr.responseType = request.responseType
xhr.timeout = request.timeout
+ for key, value of (request.headers or {})
+ xhr.setRequestHeader key, value
xhr.addEventListener 'load', ->
{status, statusText, response} = @
- if @readyState is @DONE && xhr.status is 200
- if request.responseType is 'arraybuffer'
- response = [new Uint8Array(response)...]
- contentType = @getResponseHeader 'Content-Type'
- contentDisposition = @getResponseHeader 'Content-Disposition'
- cb {status, statusText, response, contentType, contentDisposition}
- else
- cb {status, statusText, response, error: true}
+ responseHeaderString = @getAllResponseHeaders()
+ if response and request.responseType is 'arraybuffer'
+ response = [new Uint8Array(response)...]
+ cb {status, statusText, response, responseHeaderString}
, false
xhr.addEventListener 'error', ->
cb {error: true}
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index 78697a9db..3bcb24a34 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -41,67 +41,83 @@ $.extend = (object, properties) ->
return
$.ajax = do ->
- # Status Code 304: Not modified
- # With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
- # This saves a lot of bandwidth and CPU time for both the users and the servers.
- lastModified = {}
if window.wrappedJSObject and not XMLHttpRequest.wrappedJSObject
pageXHR = XPCNativeWrapper window.wrappedJSObject.XMLHttpRequest
else
pageXHR = XMLHttpRequest
- (url, options={}, extra={}) ->
- {type, whenModified, bypassCache, upCallbacks, form} = extra
- options.responseType ?= 'json' if /\.json$/.test url
+ (url, options={}) ->
+ {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options
+ responseType ?= 'json'
# XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
url = url.replace /^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'
- if whenModified
- params = []
- # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659
- params.push "s=#{whenModified}" if $.engine is 'blink'
- params.push "t=#{Date.now()}" if Site.software is 'yotsuba' and bypassCache
- url0 = url
- url += '?' + params.join('&') if params.length
r = new pageXHR()
type or= form and 'post' or 'get'
try
r.open type, url, true
- if whenModified
- r.setRequestHeader 'If-Modified-Since', lastModified[whenModified][url0] if lastModified[whenModified]?[url0]?
- $.on r, 'load', -> (lastModified[whenModified] or= {})[url0] = r.getResponseHeader 'Last-Modified'
- $.extend r, options
- $.extend r.upload, upCallbacks
+ for key, value of (headers or {})
+ r.setRequestHeader key, value
+ $.extend r, {onloadend, timeout, responseType, withCredentials}
+ $.extend r.upload, {onprogress}
# connection error or content blocker
- $.on r, 'error', -> (c.error "4chan X failed to load: #{url}" unless r.status)
+ $.on r, 'error', -> (c.warn "4chan X failed to load: #{url}" unless r.status)
+ <% if (type === 'crx') { %>
+ # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
+ $.on r, 'load', ->
+ return unless r.readyState is 4 and r.status is 200 and r.statusText is '' and r.response is null and !$.ajaxWarningShown
+ new Notice 'warning', "Error loading #{url}; try going to chrome://flags/#network-service and disabling the network service flag."
+ $.ajaxWarningShown = true
+ <% } %>
r.send form
catch err
# XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
throw err unless err.result is 0x805e0006
- for event in ['error', 'loadend']
- r["on#{event}"] = options["on#{event}"]
- $.queueTask $.event, event, null, r
+ r.onloadend = onloadend
+ $.queueTask $.event, 'error', null, r
+ $.queueTask $.event, 'loadend', null, r
r
+# Status Code 304: Not modified
+# With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
+# This saves a lot of bandwidth and CPU time for both the users and the servers.
+$.lastModified = {}
+$.whenModified = (url, bucket, cb, options={}) ->
+ {timeout, ajax} = options
+ params = []
+ # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659
+ params.push "s=#{bucket}" if $.engine is 'blink'
+ params.push "t=#{Date.now()}" if url.split('/')[2] is 'a.4cdn.org'
+ url0 = url
+ url += '?' + params.join('&') if params.length
+ headers = {}
+ if (t = $.lastModified[bucket]?[url0])?
+ headers['If-Modified-Since'] = t
+ r = (ajax or $.ajax) url, {
+ onloadend: ->
+ ($.lastModified[bucket] or= {})[url0] = @getResponseHeader('Last-Modified')
+ cb.call @
+ timeout
+ headers
+ }
+ r
+
do ->
reqs = {}
- $.cache = (url, cb, options) ->
- if req = reqs[url]
- if req.readyState is 4
- $.queueTask -> cb.call req, req.evt, true
- else
+ $.cache = (url, cb, options={}) ->
+ {ajax} = options
+ if (req = reqs[url])
+ if req.callbacks
req.callbacks.push cb
+ else
+ $.queueTask -> cb.call req, {isCached: true}
return req
- rm = -> delete reqs[url]
- try
- return if not (req = $.ajax url, options)
- catch err
- return
- $.on req, 'load', (e) ->
- @evt = e
+ onloadend = ->
+ unless @status
+ delete reqs[url]
for cb in @callbacks
- do (cb) => $.queueTask => cb.call @, e, false
+ do (cb) => $.queueTask => cb.call @, {isCached: false}
delete @callbacks
- $.on req, 'abort error', rm
+ req = (ajax or $.ajax) url, {onloadend}
req.callbacks = [cb]
reqs[url] = req
$.cleanCache = (testf) ->
@@ -413,16 +429,16 @@ $.sync = (key, cb) ->
$.forceSync = -> return
$.crxWorking = ->
- if chrome.runtime.getManifest()
- true
- else
- unless $.crxWarningShown
- msg = $.el 'div',
- <%= html('4chan X seems to have been updated. You will need to reload the page.') %>
- $.on $('a', msg), 'click', -> location.reload()
- new Notice 'warning', msg
- $.crxWarningShown = true
- false
+ try
+ if chrome.runtime.getManifest()
+ return true
+ unless $.crxWarningShown
+ msg = $.el 'div',
+ <%= html('4chan X seems to have been updated. You will need to reload the page.') %>
+ $.on $('a', msg), 'click', -> location.reload()
+ new Notice 'warning', msg
+ $.crxWarningShown = true
+ false
$.get = $.oneItemSugar (data, cb) ->
return unless $.crxWorking()
diff --git a/src/platform/CrossOrigin.coffee b/src/platform/CrossOrigin.coffee
index 179a90cde..117060199 100644
--- a/src/platform/CrossOrigin.coffee
+++ b/src/platform/CrossOrigin.coffee
@@ -14,123 +14,131 @@ CrossOrigin =
# XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
url = url.replace /^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'
<% if (type === 'crx') { %>
- eventPageRequest {type: 'ajax', url, responseType: 'arraybuffer'}, ({response, contentType, contentDisposition, error}) ->
- return cb null if error
- cb new Uint8Array(response), contentType, contentDisposition
+ eventPageRequest {type: 'ajax', url, headers, responseType: 'arraybuffer'}, ({response, responseHeaderString}) ->
+ response = new Uint8Array(response) if response
+ cb response, responseHeaderString
<% } %>
<% if (type === 'userscript') { %>
- # Use workaround for binary data in Greasemonkey versions < 3.2, in Pale Moon for all GM versions, and in JS Blocker (Safari).
- workaround = $.engine is 'gecko' and GM_info? and /^[0-2]\.|^3\.[01](?!\d)/.test(GM_info.version)
- workaround or= /PaleMoon\//.test(navigator.userAgent)
- workaround or= GM_info?.script?.includeJSB?
- options =
+ (GM?.xmlHttpRequest or GM_xmlhttpRequest)
method: "GET"
url: url
headers: headers
+ responseType: 'arraybuffer'
+ overrideMimeType: 'text/plain; charset=x-user-defined'
onload: (xhr) ->
- if workaround
+ if xhr.response instanceof ArrayBuffer
+ data = new Uint8Array xhr.response
+ else
r = xhr.responseText
data = new Uint8Array r.length
i = 0
while i < r.length
data[i] = r.charCodeAt i
i++
- else
- data = new Uint8Array xhr.response
- contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
- contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
- cb data, contentType, contentDisposition
+ cb data, xhr.responseHeaders
onerror: ->
cb null
onabort: ->
cb null
- if workaround
- options.overrideMimeType = 'text/plain; charset=x-user-defined'
- else
- options.responseType = 'arraybuffer'
- (GM?.xmlHttpRequest or GM_xmlhttpRequest) options
<% } %>
file: (url, cb) ->
- CrossOrigin.binary url, (data, contentType, contentDisposition) ->
+ CrossOrigin.binary url, (data, headers) ->
return cb null unless data?
- name = url.match(/([^\/]+)\/*$/)?[1]
+ name = url.match(/([^\/?#]+)\/*(?:$|[?#])/)?[1]
+ contentType = headers.match(/Content-Type:\s*(.*)/i)?[1]
+ contentDisposition = headers.match(/Content-Disposition:\s*(.*)/i)?[1]
mime = contentType?.match(/[^;]*/)[0] or 'application/octet-stream'
match =
contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?[1] or
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1]
if match
name = match.replace /\\"/g, '"'
- if GM_info?.script?.includeJSB?
- # Content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
+ if /^text\/plain;\s*charset=x-user-defined$/i.test(mime)
+ # In JS Blocker (Safari) content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] or 'application/octet-stream'
blob = new Blob([data], {type: mime})
blob.name = name
cb blob
+ Request: class Request
+ status: 0
+ statusText: ''
+ response: null
+ responseHeaderString: null
+ getResponseHeader: (headerName) ->
+ if !@responseHeaders? and @responseHeaderString?
+ @responseHeaders = {}
+ for header in @responseHeaderString.split('\r\n')
+ if (i = header.indexOf(':')) >= 0
+ key = header[...i].trim().toLowerCase()
+ val = header[i+1..].trim()
+ @responseHeaders[key] = val
+ (@responseHeaders or {})[headerName.toLowerCase()] ? null
+ abort: ->
+ onloadend: ->
+
# Attempts to fetch `url` in JSON format using cross-origin privileges, if available.
- # On success, calls `cb` with a `this` containing properties `status`, `statusText`, `response` and caches result.
- # On error/abort, calls `cb` with a `this` of `{}`.
- # If `bypassCache` is true, ignores previously cached results.
- json: do ->
- callbacks = {}
- results = {}
- success = (url, result) ->
- for cb in callbacks[url]
- $.queueTask -> cb.call result
- delete callbacks[url]
- results[url] = result
- failure = (url) ->
- for cb in callbacks[url]
- $.queueTask -> cb.call {}
- delete callbacks[url]
+ # Interface is a subset of that of $.ajax.
+ # Options:
+ # `onloadend` - called with the returned object as `this` on success or error/abort/timeout.
+ # `timeout` - time limit for request
+ # `headers` - request headers
+ # Returned object properties:
+ # `status` - HTTP status (0 if connection not successful)
+ # `statusText` - HTTP status text
+ # `response` - decoded response body
+ # `abort` - function for aborting the request (silently fails on some platforms)
+ # `getResponseHeader` - function for reading response headers
+ ajax: (url, options={}) ->
+ {onloadend, timeout, headers} = options
- (url, cb, bypassCache, timeout) ->
- <% if (type === 'userscript') { %>
- unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
- if bypassCache
- $.cleanCache (url2) -> url2 is url
- if (req = $.cache url, cb, responseType: 'json')
- $.on req, 'abort error', -> cb.call({})
- else
- cb.call {}
- return
- <% } %>
+ <% if (type === 'userscript') { %>
+ unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
+ return $.ajax url, options
+ <% } %>
- if bypassCache
- delete results[url]
- else
- if results[url]
- cb.call results[url]
- return
- if callbacks[url]
- callbacks[url].push cb
- return
- callbacks[url] = [cb]
+ req = new CrossOrigin.Request()
+ req.onloadend = onloadend
- <% if (type === 'userscript') { %>
- (GM?.xmlHttpRequest or GM_xmlhttpRequest)
- method: "GET"
- url: url+''
- timeout: timeout
- onload: (xhr) ->
- {status, statusText} = xhr
- try
- response = JSON.parse(xhr.responseText)
- success url, {status, statusText, response}
- catch
- failure url
- onerror: -> failure(url)
- onabort: -> failure(url)
- ontimeout: -> failure(url)
- <% } %>
- <% if (type === 'crx') { %>
- eventPageRequest {type: 'ajax', url, responseType: 'json', timeout}, (result) ->
- if result.status
- success url, result
- else
- failure url
- <% } %>
+ <% if (type === 'userscript') { %>
+ gmReq = (GM?.xmlHttpRequest or GM_xmlhttpRequest) {
+ method: 'GET'
+ url
+ headers
+ timeout
+ onload: (xhr) ->
+ try
+ response = if xhr.responseText then JSON.parse(xhr.responseText) else null
+ $.extend req, {
+ response
+ status: xhr.status
+ statusText: xhr.statusText
+ responseHeaderString: xhr.responseHeaders
+ }
+ req.onloadend()
+ onerror: -> req.onloadend()
+ onabort: -> req.onloadend()
+ ontimeout: -> req.onloadend()
+ }
+ if gmReq and typeof gmReq.abort is 'function'
+ req.abort = ->
+ try
+ gmReq.abort()
+ <% } %>
+
+ <% if (type === 'crx') { %>
+ eventPageRequest {type: 'ajax', url, responseType: 'json', headers, timeout}, (result) ->
+ if result.status
+ $.extend req, result
+ req.onloadend()
+ <% } %>
+
+ req
+
+ cache: (url, cb) ->
+ $.cache url, cb,
+ ajax: CrossOrigin.ajax
permission: (cb) ->
<% if (type === 'crx') { %>
diff --git a/src/site/SW.tinyboard.coffee b/src/site/SW.tinyboard.coffee
index 78f03b1ef..307614a99 100644
--- a/src/site/SW.tinyboard.coffee
+++ b/src/site/SW.tinyboard.coffee
@@ -9,7 +9,6 @@ SW.tinyboard =
'Image Host Rewriting'
'Index Generator'
'Announcement Hiding'
- 'Fourchan thingies'
'Resurrect Quotes'
'Quick Reply Personas'
'Quick Reply'
@@ -60,6 +59,12 @@ SW.tinyboard =
threadJSON: ({siteID, boardID, threadID}) ->
root = Conf['siteProperties'][siteID]?.root
if root then "#{root}#{boardID}/res/#{threadID}.json" else ''
+ threadsListJSON: ({siteID, boardID}) ->
+ root = Conf['siteProperties'][siteID]?.root
+ if root then "#{root}#{boardID}/threads.json" else ''
+ catalogJSON: ({siteID, boardID}) ->
+ root = Conf['siteProperties'][siteID]?.root
+ if root then "#{root}#{boardID}/catalog.json" else ''
selectors:
board: 'form[name="postcontrols"]'
diff --git a/src/site/SW.yotsuba.coffee b/src/site/SW.yotsuba.coffee
index b57c35b4e..df0dd4894 100644
--- a/src/site/SW.yotsuba.coffee
+++ b/src/site/SW.yotsuba.coffee
@@ -4,6 +4,11 @@ SW.yotsuba =
urls:
thread: ({boardID, threadID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}"
threadJSON: ({boardID, threadID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/thread/#{threadID}.json"
+ threadsListJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json"
+ archiveListJSON: ({boardID}) -> if BoardConfig.isArchived(boardID) then "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json" else ''
+ catalogJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/catalog.json"
+
+ isPrunedByAge: ({boardID}) -> boardID is 'f'
selectors:
board: '.board'
@@ -101,7 +106,7 @@ SW.yotsuba =
if g.BOARD.ID is 'f' and thread.OP.file
{file} = thread.OP
- $.ajax "#{location.protocol}//a.4cdn.org/f/thread/#{thread}.json",
+ $.ajax Site.urls.threadJSON({boardID: 'f', threadID: thread.ID}),
timeout: $.MINUTE
onloadend: ->
if @response
@@ -152,3 +157,6 @@ SW.yotsuba =
hasCORS: (url) ->
url.split('/')[...3].join('/') is location.protocol + '//a.4cdn.org'
+
+ sfwBoards: (sfw) ->
+ BoardConfig.sfwBoards(sfw)
diff --git a/src/site/Site.coffee b/src/site/Site.coffee
index 099dd81ee..52165d8a1 100644
--- a/src/site/Site.coffee
+++ b/src/site/Site.coffee
@@ -1,17 +1,19 @@
Site =
defaultProperties:
'4chan.org': {software: 'yotsuba'}
- '4channel.org': {software: 'yotsuba'}
- '4cdn.org': {software: 'yotsuba'}
+ '4channel.org': {canonical: '4chan.org'}
+ '4cdn.org': {canonical: '4chan.org'}
init: (cb) ->
$.extend Conf['siteProperties'], Site.defaultProperties
{hostname} = location
while hostname and hostname not of Conf['siteProperties']
hostname = hostname.replace(/^[^.]*\.?/, '')
- if hostname and Conf['siteProperties'][hostname].software of SW
- @set hostname
- cb()
+ if hostname
+ hostname = canonical if (canonical = Conf['siteProperties'][hostname].canonical)
+ if Conf['siteProperties'][hostname].software of SW
+ @set hostname
+ cb()
$.onExists doc, 'body', =>
for software of SW when (changes = SW[software].detect?())
changes.software = software
@@ -32,5 +34,4 @@ Site =
set: (@hostname) ->
@properties = Conf['siteProperties'][@hostname]
@software = @properties.software
- @hostname = '4chan.org' if @software is 'yotsuba'
$.extend @, SW[@software]
diff --git a/tools/sign.js b/tools/sign.js
deleted file mode 100644
index 8c09ed986..000000000
--- a/tools/sign.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var fs = require('fs');
-var crypto = require('crypto');
-var RSA = require('node-rsa');
-
-var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
-var channel = process.argv[2] || '';
-
-var privateKey = fs.readFileSync(`../${pkg.meta.path}.keys/${pkg.name}.pem`);
-var archive = fs.readFileSync(`testbuilds/${pkg.name}${channel}.crx.zip`);
-
-// https://developer.chrome.com/extensions/crx
-
-var publicKey = new RSA(privateKey).exportKey('pkcs8-public-der');
-var signature = crypto.createSign('sha1').update(archive).sign(privateKey);
-
-var header = Buffer.alloc(16);
-header.write('Cr24');
-header.writeInt32LE(2, 4);
-header.writeInt32LE(publicKey.length, 8);
-header.writeInt32LE(signature.length, 12);
-
-var crx = Buffer.concat([header, publicKey, signature, archive]);
-
-fs.writeFileSync(`testbuilds/${pkg.name}${channel}.crx`, crx);
diff --git a/tools/sign.sh b/tools/sign.sh
new file mode 100755
index 000000000..fc856cc37
--- /dev/null
+++ b/tools/sign.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+channel=$1
+mkdir -p tmp-crx
+cp -r "testbuilds/crx$channel" "tmp-crx/crx$channel"
+touch -d "$(jq -r '.date' version.json)" "tmp-crx/crx$channel"/*
+chromium --pack-extension="tmp-crx/crx$channel" --pack-extension-key="$(dirname "$PWD")/4chan-x.keys/4chan-X.pem"
+mv "tmp-crx/crx$channel.crx" "testbuilds/4chan-X$channel.crx"
+rm -r 'tmp-crx/'
diff --git a/version.json b/version.json
index f87a5ddbd..f775e69dc 100644
--- a/version.json
+++ b/version.json
@@ -1,4 +1,4 @@
{
- "version": "1.14.5.13",
- "date": "2019-03-08T23:32:11.908Z"
+ "version": "1.14.7.2",
+ "date": "2019-04-11T15:38:53.367Z"
}
\ No newline at end of file