Merge branch 'master' into postjumper

This commit is contained in:
ebinBuddha 2019-04-13 09:11:05 +02:00
commit 1b9378e809
76 changed files with 4600 additions and 3402 deletions

View File

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

View File

@ -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:<br>`git config user.name yourname`<br>`git config user.email youremail`
- Commit your changes: `git commit -a`

View File

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

View File

@ -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).

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -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"
}
]

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.14.5.13' />
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.14.7.2' />
</app>
</gupdate>

View File

@ -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"
}
]

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.14.5.13' />
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.14.7.2' />
</app>
</gupdate>

1
crx-chromium-version.txt Normal file
View File

@ -0,0 +1 @@
Chromium 73.0.3683.75 built on Debian buster/sid, running on Debian buster/sid

View File

@ -26,7 +26,7 @@
<p><strong>Private browsing</strong>: 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 <code>Remember Last Read Post</code> and <code>Remember Your Posts</code> options in the settings panel. You can clear all 4chan browsing history saved by 4chan X by resetting your settings.</p>
<h2 id="install">Install</h2>
<input hidden type="checkbox" id="firefox-hide"><div><h3 id="firefox"><label for="firefox-hide">Firefox</label></h3>
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a>, <a href="https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/">Violentmonkey</a> or <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Tampermonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/">Violentmonkey</a>, <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Tampermonkey</a>, or <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a> (issues since v4: <a href="https://github.com/greasemonkey/greasemonkey/issues/2526">#2526</a>, <a href="https://github.com/greasemonkey/greasemonkey/issues/2574">#2576</a>), then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
<p>Ports of Greasemonkey are available for <a href="https://sourceforge.net/projects/gmport/">SeaMonkey</a> and <a href="https://github.com/janekptacijarabaci/greasemonkey/releases/latest">Pale Moon</a>.</p>
</div><input hidden type="checkbox" id="chromium-hide"><div><h3 id="chromium"><label for="chromium-hide">Chromium</label></h3>
<p><strong>Userscript</strong>: Install <a href="https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag">Violentmonkey</a>) or <a href="https://tampermonkey.net/">Tampermonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>

View File

@ -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": {

View File

@ -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": {

View File

@ -1,10 +0,0 @@
<% url = (url || 'https://www.4chan.org/feedback'); %><!doctype html>
<html><head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=<%= url %>">
<title>Redirect</title>
</head>
<body>
Redirecting to <a href="<%= url %>"><%= url %></a>...
</body>
</html>

View File

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

View File

@ -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}/"

View File

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

View File

@ -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}"

View File

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

View File

@ -5,9 +5,9 @@
?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">}
<span class="name?{capcode}{ capcode}">${name}</span>
?{tripcode}{ <span class="postertrip">${tripcode}</span>}
?{o.xa19s}{ <span class="like-score">${o.xa19s}</span>}
?{pass}{ <span title="Pass user since ${pass}" class="n-pu"></span>}
?{capcode}{ <strong class="capcode hand id_${capcodeLC}" title="Highlight posts by ${capcodePlural}">## ${capcode}</strong>}
?{!capcode && typeof o.xa18 !== "undefined"}{ <strong class="capcode hand n-atb n-atb-${o.xa18} id_at${o.xa18}"></strong>}
?{email}{</a>}
?{boardID === "f" && !o.isReply || capcodeDescription}{}{ }
?{capcodeDescription}{ <img src="${staticPath}${capcodeLC}icon${gifIcon}" alt="${capcode} Icon" title="This user is ${capcodeDescription}." class="identityIcon retina">}
@ -19,6 +19,7 @@
<span class="postNum?{!(boardID === "f" && !o.isReply)}{ desktop}">
<a href="${postLink}" title="Link to this post">No.</a>
<a href="${quoteLink}" title="Reply to this post">${ID}</a>
?{o.xa19l && o.isReply}{ <a data-cmd="like-post" href="#" class="like-btn">Like! ×${o.xa19l}</a>}
?{o.isSticky}{ <img src="${staticPath}sticky${gifIcon}" alt="Sticky" title="Sticky"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="stickyIcon retina"}>}
?{o.isClosed && !o.isArchived}{ <img src="${staticPath}closed${gifIcon}" alt="Closed" title="Closed"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="closedIcon retina"}>}
?{o.isArchived}{ <img src="${staticPath}archived${gifIcon}" alt="Archived" title="Archived" class="archivedIcon retina">}

View File

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

View File

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

View File

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

View File

@ -3,20 +3,29 @@
Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions" target="_blank">regular expressions</a>, one per line.<br>
Lines starting with a <code>#</code> will be ignored.<br>
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
MD5 filtering uses exact string matching, not regular expressions.
MD5 and Unique ID filtering use exact string matching, not regular expressions.
</p>
<ul>You can use these settings with each regular expression, separate them with semicolons:
<li>
Per boards, separate them with commas. It is global if not specified. Use <code>sfw</code> and <code>nsfw</code> to reference all worksafe or not-worksafe boards.<br>
For example: <code>boards:a,jp;</code>.<br>
To specify boards on a particular site, put the beginning of the domain and a slash character before the list.<br>
Any initial <code>www.</code> should not be included, and all 4chan domains are considered <code>4chan.org</code>.<br>
For example: <code>boards:4:a,jp,sama:a,z;</code>.<br>
An asterisk can be used to specify all boards on a site.<br>
For example: <code>boards:4:*;</code>.<br>
</li>
<li>
In case of a global rule or one that uses <code>sfw</code>/<code>nsfw</code>, select boards to be excluded from the filter.<br>
Select boards to be excluded from the filter. The syntax is the same as for the <code>boards:</code> option above.<br>
For example: <code>exclude:vg,v;</code>.
</li>
<li>
Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).<br>
For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.
Filter OPs only along with their threads (`only`) or replies only (`no`).<br>
For example: <code>op:only;</code> or <code>op:no;</code>.
</li>
<li>
Filter only posts with files (`only`) or only posts without files (`no`).<br>
For example: <code>file:only;</code> or <code>file:no;</code>.
</li>
<li>
Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).<br>
@ -30,10 +39,16 @@
Highlighted OPs will have their threads put on top of the board index by default.<br>
For example: <code>top:yes;</code> or <code>top:no;</code>.
</li>
<li>
Show a desktop notification instead of hiding.<br>
For example: <code>notify;</code>.
</li>
<li>
Filters in the "General" section apply to multiple fields, by default <code>subject,name,filename,comment</code>.<br>
The fields can be specified with the <code>type</code> option, separated by commas.<br>
For example: <code>type:@{filterTypes};</code>.
For example: <code>type:@{filterTypes};</code>.<br>
Types can also be combined with a <code>+</code> sign; this indicates the filter applies to the given fields joined by newlines.<br>
For example: <code>type:filename+filesize+dimensions;</code>.<br>
</li>
</ul>
<p>

View File

@ -7,6 +7,7 @@
<option value="tripcode">Tripcode</option>
<option value="capcode">Capcode</option>
<option value="pass">Pass Date</option>
<option value="email">Email</option>
<option value="subject">Subject</option>
<option value="comment">Comment</option>
<option value="flag">Flag</option>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

@ -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&#039;t delete posts because you are <a href="//www.4chan.org/banned" target="_blank">banned</a>.') %>
@ -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: {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

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

View File

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

View File

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

View File

@ -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 '## ', ''

View File

@ -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").'

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

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

View File

@ -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 <a href="javascript:;">reload</a> 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 <a href="javascript:;">reload</a> the page.') %>
$.on $('a', msg), 'click', -> location.reload()
new Notice 'warning', msg
$.crxWarningShown = true
false
$.get = $.oneItemSugar (data, cb) ->
return unless $.crxWorking()

View File

@ -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') { %>

View File

@ -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"]'

View File

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

View File

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

View File

@ -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);

8
tools/sign.sh Executable file
View File

@ -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/'

View File

@ -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"
}