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: "
" + ((!o.isReply || boardID === "f" || subject) ? "" + E(subject || "") + " " : "") + "" + ((email) ? "" : "") + "" + E(name) + "" + ((tripcode) ? " " + E(tripcode) + "" : "") + ((pass) ? " " : "") + ((capcode) ? " ## " + E(capcode) + "" : "") + ((!capcode && typeof o.xa18 !== "undefined") ? " " : "") + ((email) ? "" : "") + ((boardID === "f" && !o.isReply || capcodeDescription) ? "" : " ") + ((capcodeDescription) ? " \""" : "") + ((uniqueID && !capcode) ? " (ID: " + E(uniqueID) + ")" : "") + ((flagCode) ? " " : "") + ((flagCodeTroll) ? " \""" : "") + " " + E(dateText) + " No." + E(ID) + "" + ((o.isSticky) ? " \"Sticky\"" : "") + ((o.isClosed && !o.isArchived) ? " \"Closed\"" : "") + ((o.isArchived) ? " \"Archived\"" : "") + ((!o.isReply && g.VIEW === "index") ? "   [Reply]" : "") + "
" + innerHTML: "
" + ((!o.isReply || boardID === "f" || subject) ? "" + E(subject || "") + " " : "") + "" + ((email) ? "" : "") + "" + E(name) + "" + ((tripcode) ? " " + E(tripcode) + "" : "") + ((o.xa19s) ? " " + E(o.xa19s) + "" : "") + ((pass) ? " " : "") + ((capcode) ? " ## " + E(capcode) + "" : "") + ((email) ? "" : "") + ((boardID === "f" && !o.isReply || capcodeDescription) ? "" : " ") + ((capcodeDescription) ? " \""" : "") + ((uniqueID && !capcode) ? " (ID: " + E(uniqueID) + ")" : "") + ((flagCode) ? " " : "") + ((flagCodeTroll) ? " \""" : "") + " " + E(dateText) + " No." + E(ID) + "" + ((o.xa19l && o.isReply) ? " Like! ×" + E(o.xa19l) + "" : "") + ((o.isSticky) ? " \"Sticky\"" : "") + ((o.isClosed && !o.isArchived) ? " \"Closed\"" : "") + ((o.isArchived) ? " \"Archived\"" : "") + ((!o.isReply && g.VIEW === "index") ? "   [Reply]" : "") + "
" }; /* 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 ×" }); $('.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: "
" + innerHTML: "
" }); 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.

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.

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: "
Thread Watcher ×
" }); @@ -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: "
" + ((!o.isReply || boardID === "f" || subject) ? "" + E(subject || "") + " " : "") + "" + ((email) ? "" : "") + "" + E(name) + "" + ((tripcode) ? " " + E(tripcode) + "" : "") + ((pass) ? " " : "") + ((capcode) ? " ## " + E(capcode) + "" : "") + ((!capcode && typeof o.xa18 !== "undefined") ? " " : "") + ((email) ? "" : "") + ((boardID === "f" && !o.isReply || capcodeDescription) ? "" : " ") + ((capcodeDescription) ? " \""" : "") + ((uniqueID && !capcode) ? " (ID: " + E(uniqueID) + ")" : "") + ((flagCode) ? " " : "") + ((flagCodeTroll) ? " \""" : "") + " " + E(dateText) + " No." + E(ID) + "" + ((o.isSticky) ? " \"Sticky\"" : "") + ((o.isClosed && !o.isArchived) ? " \"Closed\"" : "") + ((o.isArchived) ? " \"Archived\"" : "") + ((!o.isReply && g.VIEW === "index") ? "   [Reply]" : "") + "
" + innerHTML: "
" + ((!o.isReply || boardID === "f" || subject) ? "" + E(subject || "") + " " : "") + "" + ((email) ? "" : "") + "" + E(name) + "" + ((tripcode) ? " " + E(tripcode) + "" : "") + ((o.xa19s) ? " " + E(o.xa19s) + "" : "") + ((pass) ? " " : "") + ((capcode) ? " ## " + E(capcode) + "" : "") + ((email) ? "" : "") + ((boardID === "f" && !o.isReply || capcodeDescription) ? "" : " ") + ((capcodeDescription) ? " \""" : "") + ((uniqueID && !capcode) ? " (ID: " + E(uniqueID) + ")" : "") + ((flagCode) ? " " : "") + ((flagCodeTroll) ? " \""" : "") + " " + E(dateText) + " No." + E(ID) + "" + ((o.xa19l && o.isReply) ? " Like! ×" + E(o.xa19l) + "" : "") + ((o.isSticky) ? " \"Sticky\"" : "") + ((o.isClosed && !o.isArchived) ? " \"Closed\"" : "") + ((o.isArchived) ? " \"Archived\"" : "") + ((!o.isReply && g.VIEW === "index") ? "   [Reply]" : "") + "
" }; /* 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 ×" }); $('.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: "
" + innerHTML: "
" }); 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.

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.

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: "
Thread Watcher ×
" }); @@ -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: "
" + ((!o.isReply || boardID === "f" || subject) ? "" + E(subject || "") + " " : "") + "" + ((email) ? "" : "") + "" + E(name) + "" + ((tripcode) ? " " + E(tripcode) + "" : "") + ((pass) ? " " : "") + ((capcode) ? " ## " + E(capcode) + "" : "") + ((!capcode && typeof o.xa18 !== "undefined") ? " " : "") + ((email) ? "" : "") + ((boardID === "f" && !o.isReply || capcodeDescription) ? "" : " ") + ((capcodeDescription) ? " \""" : "") + ((uniqueID && !capcode) ? " (ID: " + E(uniqueID) + ")" : "") + ((flagCode) ? " " : "") + ((flagCodeTroll) ? " \""" : "") + " " + E(dateText) + " No." + E(ID) + "" + ((o.isSticky) ? " \"Sticky\"" : "") + ((o.isClosed && !o.isArchived) ? " \"Closed\"" : "") + ((o.isArchived) ? " \"Archived\"" : "") + ((!o.isReply && g.VIEW === "index") ? "   [Reply]" : "") + "
" + innerHTML: "
" + ((!o.isReply || boardID === "f" || subject) ? "" + E(subject || "") + " " : "") + "" + ((email) ? "" : "") + "" + E(name) + "" + ((tripcode) ? " " + E(tripcode) + "" : "") + ((o.xa19s) ? " " + E(o.xa19s) + "" : "") + ((pass) ? " " : "") + ((capcode) ? " ## " + E(capcode) + "" : "") + ((email) ? "" : "") + ((boardID === "f" && !o.isReply || capcodeDescription) ? "" : " ") + ((capcodeDescription) ? " \""" : "") + ((uniqueID && !capcode) ? " (ID: " + E(uniqueID) + ")" : "") + ((flagCode) ? " " : "") + ((flagCodeTroll) ? " \""" : "") + " " + E(dateText) + " No." + E(ID) + "" + ((o.xa19l && o.isReply) ? " Like! ×" + E(o.xa19l) + "" : "") + ((o.isSticky) ? " \"Sticky\"" : "") + ((o.isClosed && !o.isArchived) ? " \"Closed\"" : "") + ((o.isArchived) ? " \"Archived\"" : "") + ((!o.isReply && g.VIEW === "index") ? "   [Reply]" : "") + "
" }; /* 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 ×" }); $('.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: "
" + innerHTML: "
" }); 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.

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.

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: "
Thread Watcher ×
" }); @@ -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

-

Install Greasemonkey, Violentmonkey or Tampermonkey, then click here to install 4chan X.

+

Install Violentmonkey, Tampermonkey, or Greasemonkey (issues since v4: #2526, #2576), then click here to install 4chan X.

Ports of Greasemonkey are available for SeaMonkey and Pale Moon.

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}{ } ?{pass}{ } ?{capcode}{ ## ${capcode}} - ?{!capcode && typeof o.xa18 !== "undefined"}{ } ?{email}{} ?{boardID === "f" && !o.isReply || capcodeDescription}{}{ } ?{capcodeDescription}{ ${capcode} Icon} @@ -19,6 +19,7 @@ No. ${ID} + ?{o.xa19l && o.isReply}{ } ?{o.isSticky}{ Sticky} ?{o.isClosed && !o.isArchived}{ Closed} ?{o.isArchived}{ Archived} 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.

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 @@ + 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