diff --git a/CHANGELOG.md b/CHANGELOG.md index e842d860f..690359d11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ -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.6 + +**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.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)] diff --git a/builds/4chan-X-beta.crx b/builds/4chan-X-beta.crx index c07e86681..a6a3eee18 100644 Binary files a/builds/4chan-X-beta.crx and b/builds/4chan-X-beta.crx differ diff --git a/builds/4chan-X-beta.meta.js b/builds/4chan-X-beta.meta.js index 72aed01f8..9c79379f8 100644 --- a/builds/4chan-X-beta.meta.js +++ b/builds/4chan-X-beta.meta.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X beta -// @version 1.14.5.14 +// @version 1.14.6.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X diff --git a/builds/4chan-X-beta.user.js b/builds/4chan-X-beta.user.js index 686fa38d1..13b2a8bc8 100644 --- a/builds/4chan-X-beta.user.js +++ b/builds/4chan-X-beta.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X beta -// @version 1.14.5.14 +// @version 1.14.6.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X @@ -198,7 +198,7 @@ docSet = function() { }; g = { - VERSION: '1.14.5.14', + VERSION: '1.14.6.0', NAMESPACE: '4chan X.', boards: {} }; @@ -241,6 +241,7 @@ Config = (function() { 'JSON Index': [true, 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'], 'Use 4chan X Catalog': [true, 'Link to 4chan X\'s catalog instead of the native 4chan one.', 1], 'Index Refresh Notifications': [false, 'Show a notice at the top of the page when the index is refreshed.', 1], + 'Follow Cursor': [true, 'Image Hover and Quote Preview move with the mouse cursor.'], 'Open Threads in New Tab': [false, 'Make links to threads in the index / 4chan X catalog open in a new tab.'], 'External Catalog': [false, 'Link to external catalog instead of the internal one.'], 'Catalog Links': [false, 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'], @@ -1390,6 +1391,10 @@ body.is_catalog .thread > a > img {\n\ .nwsb {\n\ display: inline;\n\ }\n\ +.fileText {\n\ + max-width: auto;\n\ + white-space: normal;\n\ +}\n\ /* Ads */\n\ .ad-cnt > *, .adg-rects > *, .bsa-cnt {\n\ height: auto !important;\n\ @@ -2587,6 +2592,13 @@ span.hide-announcement {\n\ .fileThumb > .warning {\n\ clear: both;\n\ }\n\ +#ihover {\n\ + pointer-events: none;\n\ + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */\n\ + max-height: 95vh;\n\ + max-height: calc(100vh - 25px);\n\ + max-width: 100vw;\n\ +}\n\ /* WEBM Metadata */\n\ .webm-title > a::before {\n\ content: \"title\";\n\ @@ -4595,58 +4607,43 @@ $ = (function() { }; $.ajax = (function() { - var lastModified, pageXHR; - lastModified = {}; + var pageXHR; if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) { pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest); } else { pageXHR = XMLHttpRequest; } - return function(url, options, extra) { - var bypassCache, err, event, form, j, len, params, r, ref, ref1, type, upCallbacks, url0, whenModified; + return function(url, options) { + var err, form, headers, key, onloadend, onprogress, r, ref, responseType, timeout, type, value, withCredentials; if (options == null) { options = {}; } - if (extra == null) { - extra = {}; - } - type = extra.type, whenModified = extra.whenModified, bypassCache = extra.bypassCache, upCallbacks = extra.upCallbacks, form = extra.form; - if (/\.json$/.test(url)) { - if (options.responseType == null) { - options.responseType = 'json'; - } + onloadend = options.onloadend, timeout = options.timeout, responseType = options.responseType, withCredentials = options.withCredentials, type = options.type, onprogress = options.onprogress, form = options.form, headers = options.headers; + if (responseType == null) { + responseType = 'json'; } url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'); - if (whenModified) { - params = []; - if ($.engine === 'blink') { - params.push("s=" + whenModified); - } - if (Site.software === 'yotsuba' && bypassCache) { - params.push("t=" + (Date.now())); - } - url0 = url; - if (params.length) { - url += '?' + params.join('&'); - } - } r = new pageXHR(); type || (type = form && 'post' || 'get'); try { r.open(type, url, true); - if (whenModified) { - if (((ref = lastModified[whenModified]) != null ? ref[url0] : void 0) != null) { - r.setRequestHeader('If-Modified-Since', lastModified[whenModified][url0]); - } - $.on(r, 'load', function() { - return (lastModified[whenModified] || (lastModified[whenModified] = {}))[url0] = r.getResponseHeader('Last-Modified'); - }); + ref = headers || {}; + for (key in ref) { + value = ref[key]; + r.setRequestHeader(key, value); } - $.extend(r, options); - $.extend(r.upload, upCallbacks); + $.extend(r, { + onloadend: onloadend, + timeout: timeout, + responseType: responseType, + withCredentials: withCredentials + }); + $.extend(r.upload, { + onprogress: onprogress + }); $.on(r, 'error', function() { if (!r.status) { - return c.error("4chan X failed to load: " + url); + return c.warn("4chan X failed to load: " + url); } }); r.send(form); @@ -4655,51 +4652,82 @@ $ = (function() { if (err.result !== 0x805e0006) { throw err; } - ref1 = ['error', 'loadend']; - for (j = 0, len = ref1.length; j < len; j++) { - event = ref1[j]; - r["on" + event] = options["on" + event]; - $.queueTask($.event, event, null, r); - } + r.onloadend = onloadend; + $.queueTask($.event, 'error', null, r); + $.queueTask($.event, 'loadend', null, r); } return r; }; })(); + $.lastModified = {}; + + $.whenModified = function(url, bucket, cb, options) { + var ajax, headers, params, r, ref, t, timeout, url0; + if (options == null) { + options = {}; + } + timeout = options.timeout, ajax = options.ajax; + params = []; + if ($.engine === 'blink') { + params.push("s=" + bucket); + } + if (Site.software === 'yotsuba') { + params.push("t=" + (Date.now())); + } + url0 = url; + if (params.length) { + url += '?' + params.join('&'); + } + headers = {}; + if ((t = (ref = $.lastModified[bucket]) != null ? ref[url0] : void 0) != null) { + headers['If-Modified-Since'] = t; + } + r = (ajax || $.ajax)(url, { + onloadend: function() { + var base; + ((base = $.lastModified)[bucket] || (base[bucket] = {}))[url0] = this.getResponseHeader('Last-Modified'); + return cb.call(this); + }, + timeout: timeout, + headers: headers + }); + return r; + }; + (function() { var reqs; reqs = {}; $.cache = function(url, cb, options) { - var err, req, rm; - if (req = reqs[url]) { - if (req.readyState === 4) { - $.queueTask(function() { - return cb.call(req, req.evt, true); - }); - } else { + var ajax, onloadend, req; + if (options == null) { + options = {}; + } + ajax = options.ajax; + if ((req = reqs[url])) { + if (req.callbacks) { req.callbacks.push(cb); + } else { + $.queueTask(function() { + return cb.call(req, { + isCached: true + }); + }); } return req; } - rm = function() { - return delete reqs[url]; - }; - try { - if (!(req = $.ajax(url, options))) { - return; - } - } catch (_error) { - err = _error; - return; - } - $.on(req, 'load', function(e) { + onloadend = function() { var fn1, j, len, ref; - this.evt = e; + if (!this.status) { + delete reqs[url]; + } ref = this.callbacks; fn1 = (function(_this) { return function(cb) { return $.queueTask(function() { - return cb.call(_this, e, false); + return cb.call(_this, { + isCached: false + }); }); }; })(this); @@ -4708,8 +4736,10 @@ $ = (function() { fn1(cb); } return delete this.callbacks; + }; + req = (ajax || $.ajax)(url, { + onloadend: onloadend }); - $.on(req, 'abort error', rm); req.callbacks = [cb]; return reqs[url] = req; }; @@ -5402,7 +5432,7 @@ $$ = (function() { }).call(this); CrossOrigin = (function() { - var CrossOrigin; + var CrossOrigin, Request; CrossOrigin = { binary: function(url, cb, headers) { @@ -5471,95 +5501,94 @@ CrossOrigin = (function() { return cb(blob); }); }, - json: (function() { - var callbacks, failure, results, success; - callbacks = {}; - results = {}; - success = function(url, result) { - var cb, j, len, ref; - ref = callbacks[url]; - for (j = 0, len = ref.length; j < len; j++) { - cb = ref[j]; - $.queueTask(function() { - return cb.call(result); - }); - } - delete callbacks[url]; - return results[url] = result; - }; - failure = function(url) { - var cb, j, len, ref; - ref = callbacks[url]; - for (j = 0, len = ref.length; j < len; j++) { - cb = ref[j]; - $.queueTask(function() { - return cb.call({}); - }); - } - return delete callbacks[url]; - }; - return function(url, cb, bypassCache, timeout) { - var req; - if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) { - if (bypassCache) { - $.cleanCache(function(url2) { - return url2 === url; - }); - } - if ((req = $.cache(url, cb, { - responseType: 'json' - }))) { - $.on(req, 'abort error', function() { - return cb.call({}); - }); - } else { - cb.call({}); - } - return; - } - if (bypassCache) { - delete results[url]; - } else { - if (results[url]) { - cb.call(results[url]); - return; - } - if (callbacks[url]) { - callbacks[url].push(cb); - return; - } - } - callbacks[url] = [cb]; - return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({ - method: "GET", - url: url + '', - timeout: timeout, - onload: function(xhr) { - var response, status, statusText; - status = xhr.status, statusText = xhr.statusText; - try { - response = JSON.parse(xhr.responseText); - return success(url, { - status: status, - statusText: statusText, - response: response - }); - } catch (_error) { - return failure(url); + Request: Request = (function() { + function Request() {} + + Request.prototype.status = 0; + + Request.prototype.statusText = ''; + + Request.prototype.response = null; + + Request.prototype.responseHeaderString = null; + + Request.prototype.getResponseHeader = function(headerName) { + var header, i, j, key, len, ref, ref1, val; + if ((this.responseHeaders == null) && (this.responseHeaderString != null)) { + this.responseHeaders = {}; + ref = this.responseHeaderString.split('\r\n'); + for (j = 0, len = ref.length; j < len; j++) { + header = ref[j]; + if ((i = header.indexOf(':')) >= 0) { + key = header.slice(0, i).trim().toLowerCase(); + val = header.slice(i + 1).trim(); + this.responseHeaders[key] = val; } - }, - onerror: function() { - return failure(url); - }, - onabort: function() { - return failure(url); - }, - ontimeout: function() { - return failure(url); } - }); + } + return (ref1 = (this.responseHeaders || {})[headerName.toLowerCase()]) != null ? ref1 : null; }; + + Request.prototype.abort = function() {}; + + Request.prototype.onloadend = function() {}; + + return Request; + })(), + ajax: function(url, options) { + var gmReq, headers, onloadend, req, timeout; + if (options == null) { + options = {}; + } + onloadend = options.onloadend, timeout = options.timeout, headers = options.headers; + if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) { + return $.ajax(url, options); + } + req = new CrossOrigin.Request(); + req.onloadend = onloadend; + gmReq = ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({ + method: 'GET', + url: url, + headers: headers, + timeout: timeout, + onload: function(xhr) { + var response; + try { + response = xhr.responseText ? JSON.parse(xhr.responseText) : null; + $.extend(req, { + response: response, + status: xhr.status, + statusText: xhr.statusText, + responseHeaderString: xhr.responseHeaders + }); + } catch (_error) {} + return req.onloadend(); + }, + onerror: function() { + return req.onloadend(); + }, + onabort: function() { + return req.onloadend(); + }, + ontimeout: function() { + return req.onloadend(); + } + }); + if (typeof gmReq.abort === 'function') { + req.abort = function() { + try { + return gmReq.abort(); + } catch (_error) {} + }; + } + return req; + }, + cache: function(url, cb) { + return $.cache(url, cb, { + ajax: CrossOrigin.ajax + }); + }, permission: function(cb) { return cb(); } @@ -5764,7 +5793,7 @@ DataBoard = (function() { bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; DataBoard = (function() { - DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']; + DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']; function DataBoard(key1, sync, dontClean) { var init; @@ -5952,7 +5981,7 @@ DataBoard = (function() { boardID: boardID, threadID: threadID, postID: postID, - val: {} + defaultValue: {} }); ref = rm || []; for (i = 0, len = ref.length; i < len; i++) { @@ -5971,10 +6000,13 @@ DataBoard = (function() { })(this), cb); }; - DataBoard.prototype.setLastChecked = function() { + DataBoard.prototype.setLastChecked = function(key) { + if (key == null) { + key = 'lastChecked'; + } return this.save((function(_this) { return function() { - return _this.data.lastChecked = Date.now(); + return _this.data[key] = Date.now(); }; })(this)); }; @@ -6005,9 +6037,6 @@ DataBoard = (function() { DataBoard.prototype.clean = function() { var boardID, now, ref, ref1, siteID, val; - if (Site.software !== 'yotsuba') { - return; - } siteID = Site.hostname; ref = this.data[siteID].boards; for (boardID in ref) { @@ -6027,21 +6056,36 @@ DataBoard = (function() { }; DataBoard.prototype.ajaxClean = function(boardID) { - return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/threads.json", (function(_this) { - return function(e1) { - var response1; - if (e1.target.status !== 200) { + var base, siteID, that, threadsList; + that = this; + siteID = Site.hostname; + threadsList = typeof (base = Site.urls).threadsListJSON === "function" ? base.threadsListJSON({ + siteID: siteID, + boardID: boardID + }) : void 0; + if (!threadsList) { + return; + } + return $.cache(threadsList, function() { + var archiveList, base1, response1; + if (this.status !== 200) { + return; + } + archiveList = typeof (base1 = Site.urls).archiveListJSON === "function" ? base1.archiveListJSON({ + siteID: siteID, + boardID: boardID + }) : void 0; + if (!archiveList) { + return that.ajaxCleanParse(boardID, this.response); + } + response1 = this.response; + return $.cache(archiveList, function() { + if (this.status !== 200) { return; } - response1 = e1.target.response; - return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/archive.json", function(e2) { - if (!(e2.target.status === 200 || (boardID === 'b' || boardID === 'f' || boardID === 'trash' || boardID === 'bant'))) { - return; - } - return _this.ajaxCleanParse(boardID, response1, e2.target.response); - }); - }; - })(this)); + return that.ajaxCleanParse(boardID, response1, this.response); + }); + }); }; DataBoard.prototype.ajaxCleanParse = function(boardID, response1, response2) { @@ -6102,7 +6146,7 @@ Fetcher = (function() { Fetcher = (function() { function Fetcher(boardID1, threadID, postID1, root, quoter) { - var board, post, ref, thread; + var board, post, ref, that, thread; this.boardID = boardID1; this.threadID = threadID; this.postID = postID1; @@ -6122,11 +6166,15 @@ Fetcher = (function() { } this.root.textContent = "Loading post No." + this.postID + "..."; if (this.threadID) { - $.cache(location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json", (function(_this) { - return function(e, isCached) { - return _this.fetchedPost(e.target, isCached); - }; - })(this)); + that = this; + $.cache(Site.urls.threadJSON({ + boardID: this.boardID, + threadID: this.threadID + }), function(arg) { + var isCached; + isCached = arg.isCached; + return that.fetchedPost(this, isCached); + }); } else { this.archivedPost(); } @@ -6165,18 +6213,18 @@ Fetcher = (function() { }; Fetcher.prototype.fetchedPost = function(req, isCached) { - var api, board, k, len, post, posts, status, thread; + var api, board, k, len, post, posts, status, that, thread; if (post = g.posts[this.boardID + "." + this.postID]) { this.insert(post); return; } status = req.status; if (status !== 200) { - if (this.archivedPost()) { + if (status && this.archivedPost()) { return; } $.addClass(this.root, 'warning'); - this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : "Error " + req.statusText + " (" + req.status + ")."; + this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : !status ? 'Connection Error' : "Error " + req.statusText + " (" + req.status + ")."; return; } posts = req.response.posts; @@ -6189,15 +6237,17 @@ Fetcher = (function() { } if (post.no !== this.postID) { if (isCached) { - api = location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json"; + api = Site.urls.threadJSON({ + boardID: this.boardID, + threadID: this.threadID + }); $.cleanCache(function(url) { return url === api; }); - $.cache(api, (function(_this) { - return function(e) { - return _this.fetchedPost(e.target, false); - }; - })(this)); + that = this; + $.cache(api, function() { + return that.fetchedPost(this, false); + }); return; } if (this.archivedPost()) { @@ -6230,7 +6280,7 @@ Fetcher = (function() { encryptionOK = /^https:\/\//.test(url) || location.protocol === 'http:'; if (encryptionOK || Conf['Exempt Archives from Encryption']) { that = this; - CrossOrigin.json(url, function() { + CrossOrigin.cache(url, function() { var key, media, ref, ref1; if (!encryptionOK && ((ref = this.response) != null ? ref.media : void 0)) { media = this.response.media; @@ -7359,6 +7409,26 @@ SW = {}; } else { return ''; } + }, + threadsListJSON: function(arg) { + var boardID, ref, root, siteID; + siteID = arg.siteID, boardID = arg.boardID; + root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0; + if (root) { + return "" + root + boardID + "/threads.json"; + } else { + return ''; + } + }, + catalogJSON: function(arg) { + var boardID, ref, root, siteID; + siteID = arg.siteID, boardID = arg.boardID; + root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0; + if (root) { + return "" + root + boardID + "/catalog.json"; + } else { + return ''; + } } }, selectors: { @@ -7475,6 +7545,25 @@ SW = {}; var boardID, threadID; boardID = arg.boardID, threadID = arg.threadID; return location.protocol + "//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json"; + }, + threadsListJSON: function(arg) { + var boardID; + boardID = arg.boardID; + return location.protocol + "//a.4cdn.org/" + boardID + "/threads.json"; + }, + archiveListJSON: function(arg) { + var boardID; + boardID = arg.boardID; + if (BoardConfig.isArchived(boardID)) { + return location.protocol + "//a.4cdn.org/" + boardID + "/archive.json"; + } else { + return ''; + } + }, + catalogJSON: function(arg) { + var boardID; + boardID = arg.boardID; + return location.protocol + "//a.4cdn.org/" + boardID + "/catalog.json"; } }, selectors: { @@ -7565,7 +7654,10 @@ SW = {}; thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : void 0; if (g.BOARD.ID === 'f' && thread.OP.file) { file = thread.OP.file; - return $.ajax(location.protocol + "//a.4cdn.org/f/thread/" + thread + ".json", { + return $.ajax(Site.urls.threadJSON({ + boardID: 'f', + threadID: thread.ID + }), { timeout: $.MINUTE, onloadend: function() { if (this.response) { @@ -7860,7 +7952,9 @@ Redirect = (function() { response: response }); } else { - CrossOrigin.json(url, load(i), true); + CrossOrigin.ajax(url, { + onloadend: load(i) + }); } } } else { @@ -8025,7 +8119,7 @@ Filter = (function() { filters: {}, results: {}, init: function() { - var base, base1, boards, err, excludes, filter, hl, i, j, key, len, len1, line, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, sfwBoards, stub, top, type, types; + var base, base1, board, boards, boardsRaw, err, excludes, excludesRaw, file, filter, hide, hl, i, isstring, j, k, key, l, len, len1, len2, len3, line, mask, noti, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, regexp, sfwBoards, stub, top, type, types; if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) { return; } @@ -8041,16 +8135,33 @@ Filter = (function() { if (line[0] === '#') { continue; } - if (!(regexp = line.match(/\/(.+)\/(\w*)/))) { + if (!(regexp = line.match(/\/(.*)\/(\w*)/))) { continue; } filter = line.replace(regexp[0], ''); - boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global'; - boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards); - boards = boards === 'global' ? null : boards.split(','); - excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase() : void 0) || null; - excludes = excludes === null ? null : excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); - if (key === 'uniqueID' || key === 'MD5') { + boardsRaw = (ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0; + if (boardsRaw) { + boards = {}; + ref3 = boardsRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); + for (j = 0, len1 = ref3.length; j < len1; j++) { + board = ref3[j]; + boards[board] = true; + } + } else { + boards = false; + } + excludesRaw = (ref4 = filter.match(/exclude:([^;]+)/)) != null ? ref4[1].toLowerCase() : void 0; + if (excludesRaw) { + excludes = {}; + ref5 = excludesRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); + for (k = 0, len2 = ref5.length; k < len2; k++) { + board = ref5[k]; + excludes[board] = true; + } + } else { + excludes = false; + } + if ((isstring = (key === 'uniqueID' || key === 'MD5'))) { regexp = regexp[1]; } else { try { @@ -8061,10 +8172,19 @@ Filter = (function() { continue; } } - op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes'; + op = ((ref6 = filter.match(/[^t]op:(no|only)/)) != null ? ref6[1] : void 0) || ''; + mask = { + 'no': 1, + 'only': 2 + }[op] || 0; + file = ((ref7 = filter.match(/file:(no|only)/)) != null ? ref7[1] : void 0) || ''; + mask = mask | ({ + 'no': 4, + 'only': 8 + }[file] || 0); stub = (function() { - var ref5; - switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) { + var ref8; + switch ((ref8 = filter.match(/stub:(yes|no)/)) != null ? ref8[1] : void 0) { case 'yes': return true; case 'no': @@ -8073,9 +8193,10 @@ Filter = (function() { return Conf['Stubs']; } })(); - if (hl = /highlight/.test(filter)) { - hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight'; - top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes'; + noti = /notify/.test(filter); + if ((hl = /highlight/.test(filter))) { + hl = ((ref8 = filter.match(/highlight:([\w-]+)/)) != null ? ref8[1] : void 0) || 'filter-highlight'; + top = ((ref9 = filter.match(/top:(yes|no)/)) != null ? ref9[1] : void 0) || 'yes'; top = top === 'yes'; } if (key === 'general') { @@ -8087,10 +8208,22 @@ Filter = (function() { types = ['subject', 'name', 'filename', 'comment']; } } - filter = this.createFilter(regexp, boards, excludes, op, stub, hl, top); + hide = !(hl || noti); + filter = { + isstring: isstring, + regexp: regexp, + boards: boards, + excludes: excludes, + mask: mask, + hide: hide, + stub: stub, + hl: hl, + top: top, + noti: noti + }; if (key === 'general') { - for (j = 0, len1 = types.length; j < len1; j++) { - type = types[j]; + for (l = 0, len3 = types.length; l < len3; l++) { + type = types[l]; ((base = this.filters)[type] || (base[type] = [])).push(filter); } } else { @@ -8106,37 +8239,8 @@ Filter = (function() { cb: this.node }); }, - createFilter: function(regexp, boards, excludes, op, stub, hl, top) { - var settings, test; - test = typeof regexp === 'string' ? function(value) { - return regexp === value; - } : function(value) { - return regexp.test(value); - }; - settings = { - hide: !hl, - stub: stub, - "class": hl, - top: top - }; - return function(value, boardID, isReply) { - if (boards && indexOf.call(boards, boardID) < 0) { - return false; - } - if (excludes && indexOf.call(excludes, boardID) >= 0) { - return false; - } - if (isReply && op === 'only' || !isReply && op === 'no') { - return false; - } - if (!test(value)) { - return false; - } - return settings; - }; - }, test: function(post, hideable) { - var filter, hide, hl, i, key, len, ref, ref1, result, stub, top, value; + var filter, hide, hl, i, key, len, mask, noti, ref, ref1, stub, top, value; if (hideable == null) { hideable = true; } @@ -8147,25 +8251,32 @@ Filter = (function() { stub = true; hl = void 0; top = false; + noti = false; if (QuoteYou.isYou(post)) { hideable = false; } + mask = (post.isReply ? 2 : 1); + mask = mask | (post.file ? 4 : 8); for (key in Filter.filters) { if (((value = Filter[key](post)) != null)) { ref = Filter.filters[key]; for (i = 0, len = ref.length; i < len; i++) { filter = ref[i]; - if ((result = filter(value, post.boardID, post.isReply))) { - if (result.hide) { - if (hideable) { - hide = true; - stub && (stub = result.stub); - } - } else { - if (!(hl && (ref1 = result["class"], indexOf.call(hl, ref1) >= 0))) { - (hl || (hl = [])).push(result["class"]); - } - top || (top = result.top); + if ((filter.boards && !filter.boards[post.boardID]) || (filter.excludes && filter.excludes[post.boardID]) || (filter.mask & mask) || (filter.isstring ? filter.regexp !== value : !filter.regexp.test(value))) { + continue; + } + if (filter.hide) { + if (hideable) { + hide = true; + stub && (stub = filter.stub); + } + } else { + if (!(hl && (ref1 = filter.hl, indexOf.call(hl, ref1) >= 0))) { + (hl || (hl = [])).push(filter.hl); + } + top || (top = filter.top); + if (filter.noti) { + noti = true; } } } @@ -8179,16 +8290,17 @@ Filter = (function() { } else { return { hl: hl, - top: top + top: top, + noti: noti }; } }, node: function() { - var hide, hl, ref, stub, top; + var hide, hl, noti, ref, stub, top; if (this.isClone) { return; } - ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top; + ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top, noti = ref.noti; if (hide) { if (this.isReply) { PostHiding.hide(this, stub); @@ -8201,6 +8313,9 @@ Filter = (function() { $.addClass.apply($, [this.nodes.root].concat(slice.call(hl))); } } + if (noti && Unread.posts && (this.ID > Unread.lastReadPost) && !QuoteYou.isYou(this)) { + return Unread.openNotification(this, ' triggered a notification filter'); + } }, isHidden: function(post) { return !!Filter.test(post).hide; @@ -8212,7 +8327,7 @@ Filter = (function() { return post.info.name; }, uniqueID: function(post) { - return post.info.uniqueID; + return post.info.uniqueID || ''; }, tripcode: function(post) { return post.info.tripcode; @@ -9137,6 +9252,11 @@ BoardConfig = (function() { domain: function(board) { return "boards." + (BoardConfig.isSFW(board) ? '4channel' : '4chan') + ".org"; }, + isArchived: function(board) { + var data; + data = (this.boards || Conf['boardConfig'].boards)[board]; + return !data || data.is_archived; + }, noAudio: function(boardID) { var boards; if (Site.software !== 'yotsuba') { @@ -9904,7 +10024,7 @@ Header = (function() { } } if (/-expired/.test(t)) { - if (boardID !== 'b' && boardID !== 'f' && boardID !== 'trash' && boardID !== 'bant') { + if (BoardConfig.isArchived(boardID)) { a.href = "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/archive"; } else { return a.firstChild; @@ -10230,7 +10350,7 @@ Index = (function() { showHiddenThreads: false, changed: {}, init: function() { - var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, select, sortEntry, tRaw, watchSettings; + var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, select, sortEntry, tRaw, watchSettings; if (!(g.VIEW === 'index' && g.BOARD.ID !== 'f')) { return; } @@ -10320,7 +10440,7 @@ Index = (function() { innerHTML: "Index Catalog Archive Bottom ×" }); $('.cataloglink a', this.navLinks).href = CatalogLinks.catalog(); - if ((ref5 = g.BOARD.ID) === 'b' || ref5 === 'trash' || ref5 === 'bant') { + if (!BoardConfig.isArchived(g.BOARD.ID)) { $('.archlistlink', this.navLinks).hidden = true; } $.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront); @@ -10339,9 +10459,9 @@ Index = (function() { $.on(this.selectSort, 'change', this.cb.sort); $.on(this.selectSize, 'change', $.cb.value); $.on(this.selectSize, 'change', this.cb.size); - ref6 = [this.selectMode, this.selectSize]; - for (k = 0, len1 = ref6.length; k < len1; k++) { - select = ref6[k]; + ref5 = [this.selectMode, this.selectSize]; + for (k = 0, len1 = ref5.length; k < len1; k++) { + select = ref5[k]; select.value = Conf[select.name]; } this.selectRev.checked = /-rev$/.test(Index.currentSort); @@ -10350,12 +10470,12 @@ Index = (function() { this.lastLongInputs = $$('input', this.lastLongOptions); this.lastLongThresholds = [0, 0]; this.lastLongOptions.hidden = this.selectSort.value !== 'lastlong'; - ref7 = this.lastLongInputs; - for (i = l = 0, len2 = ref7.length; l < len2; i = ++l) { - input = ref7[i]; + ref6 = this.lastLongInputs; + for (i = l = 0, len2 = ref6.length; l < len2; i = ++l) { + input = ref6[i]; $.on(input, 'change', this.cb.lastLongThresholds); tRaw = Conf["Last Long Reply Thresholds " + i]; - input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref8 = tRaw[g.BOARD.ID]) != null ? ref8 : 100 : tRaw; + input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref7 = tRaw[g.BOARD.ID]) != null ? ref7 : 100 : tRaw; } this.root = $.el('div', { className: 'board json-index' @@ -10376,7 +10496,7 @@ Index = (function() { return d.title = d.title.replace(/\ -\ Page\ \d+/, ''); }); $.onExists(doc, '.board > .thread > .postContainer, .board + *', function() { - var board, el, len3, m, ref9, timeEl, topNavPos; + var board, el, len3, m, ref8, timeEl, topNavPos; Build.hat = $('.board > .thread > img:first-child'); if (Build.hat) { g.BOARD.threads.forEach(function(thread) { @@ -10395,9 +10515,9 @@ Index = (function() { try { d.implementation.createDocument(null, null, null).appendChild(board); } catch (_error) {} - ref9 = $$('.navLinks'); - for (m = 0, len3 = ref9.length; m < len3; m++) { - el = ref9[m]; + ref8 = $$('.navLinks'); + for (m = 0, len3 = ref8.length; m < len3; m++) { + el = ref8[m]; $.rm(el); } $.rm($.id('ctrl-top')); @@ -10969,57 +11089,42 @@ Index = (function() { return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? '1 hidden thread' : hiddenCount + " hidden threads"; }, update: function(firstTime) { - var now, ref, ref1; - if ((ref = Index.req) != null) { - ref.abort(); + var oldReq; + if ((oldReq = Index.req)) { + delete Index.req; + oldReq.abort(); } - if ((ref1 = Index.notice) != null) { - ref1.close(); - } - if (Conf['Index Refresh Notifications'] && d.readyState !== 'loading') { - Index.notice = new Notice('info', 'Refreshing index...'); + if (Conf['Index Refresh Notifications']) { + Index.notice || (Index.notice = new Notice('info', 'Refreshing index...')); } else { - now = Date.now(); - $.ready(function() { - return Index.nTimeout = setTimeout((function() { - if (Index.req && !Index.notice) { - return Index.notice = new Notice('info', 'Refreshing index...'); - } - }), 3 * $.SECOND - (Date.now() - now)); - }); + Index.nTimeout || (Index.nTimeout = setTimeout(function() { + return Index.notice || (Index.notice = new Notice('info', 'Refreshing index...')); + }, 3 * $.SECOND)); } if (!firstTime && d.readyState !== 'loading' && !$('.board + *')) { location.reload(); return; } - Index.req = $.ajax(location.protocol + "//a.4cdn.org/" + g.BOARD + "/catalog.json", { - onabort: Index.load, - onloadend: Index.load - }, { - whenModified: 'Index', - bypassCache: true - }); + Index.req = $.whenModified(Site.urls.catalogJSON({ + boardID: g.BOARD.ID + }), 'Index', Index.load); return $.addClass(Index.button, 'fa-spin'); }, - load: function(e) { - var err, nTimeout, notice, ref, req, timeEl; + load: function() { + var err, nTimeout, notice, ref, timeEl; + if (this !== Index.req) { + return; + } $.rmClass(Index.button, 'fa-spin'); - req = Index.req, notice = Index.notice, nTimeout = Index.nTimeout; + notice = Index.notice, nTimeout = Index.nTimeout; if (nTimeout) { clearTimeout(nTimeout); } delete Index.nTimeout; delete Index.req; delete Index.notice; - if (e.type === 'abort') { - req.onloadend = null; - if (notice != null) { - notice.close(); - } - return; - } - if ((ref = req.status) !== 200 && ref !== 304) { - err = "Index refresh failed. " + (req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error'); + if ((ref = this.status) !== 200 && ref !== 304) { + err = "Index refresh failed. " + (this.status ? "Error " + this.statusText + " (" + this.status + ")" : 'Connection Error'); if (notice) { notice.setType('warning'); notice.el.lastElementChild.textContent = err; @@ -11030,9 +11135,9 @@ Index = (function() { return; } try { - if (req.status === 200) { - Index.parse(req.response); - } else if (req.status === 304) { + if (this.status === 200) { + Index.parse(this.response); + } else if (this.status === 304) { Index.pageLoad(); } } catch (_error) { @@ -11057,7 +11162,7 @@ Index = (function() { } } timeEl = $('#index-last-refresh time', Index.navLinks); - timeEl.dataset.utc = Date.parse(req.getResponseHeader('Last-Modified')); + timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified')); return RelativeDates.update(timeEl); }, parse: function(pages) { @@ -11910,20 +12015,27 @@ Settings = (function() { return $.after($('input[name="Stubs"]', section).parentNode.parentNode, div); }, "export": function() { - return $.get(Conf, function(Conf) { - delete Conf['boardConfig']; + var Conf2; + Conf2 = {}; + $.extend(Conf2, Conf); + return $.get(Conf2, function(Conf2) { + delete Conf2['boardConfig']; return Settings.downloadExport({ version: g.VERSION, date: Date.now(), - Conf: Conf + Conf: Conf2 }); }); }, downloadExport: function(data) { - var a, p; + var a, blob, p, url; + blob = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json' + }); + url = URL.createObjectURL(blob); a = $.el('a', { download: "4chan X v" + g.VERSION + "-" + data.date + ".json", - href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))))) + href: url }); p = $('.imp-exp-result', Settings.dialog); $.rmAll(p); @@ -12413,7 +12525,7 @@ Settings = (function() { }; }); $.extend(div, { - innerHTML: "
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.

Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.

" + innerHTML: "
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 and Unique ID filtering use exact string matching, not regular expressions.

Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.

" }); return $('.warning', div).hidden = Conf['Filter']; }, @@ -13120,8 +13232,9 @@ UI = (function() { }; hoverstart = function(arg) { - var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root; - root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove; + var cb, el, endEvents, height, latestEvent, noRemove, o, rect, ref, root, width; + root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, width = arg.width, cb = arg.cb, noRemove = arg.noRemove; + rect = root.getBoundingClientRect(); o = { root: root, el: el, @@ -13133,7 +13246,10 @@ UI = (function() { clientHeight: doc.clientHeight, clientWidth: doc.clientWidth, height: height, - noRemove: noRemove + width: width, + noRemove: noRemove, + clientX: (rect.left + rect.right) / 2, + clientY: (rect.top + rect.bottom) / 2 }; o.hover = hover.bind(o); o.hoverend = hoverend.bind(o); @@ -13161,16 +13277,22 @@ UI = (function() { hoverstart.padding = 25; hover = function(e) { - var clientX, clientY, height, left, ref, right, style, threshold, top; + var clientX, clientY, height, left, marginX, ref, ref1, right, style, threshold, top, width; this.latestEvent = e; height = (this.height || this.el.offsetHeight) + hoverstart.padding; - clientX = e.clientX, clientY = e.clientY; + width = this.width || this.el.offsetWidth; + ref = Conf['Follow Cursor'] ? e : this, clientX = ref.clientX, clientY = ref.clientY; top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120)); threshold = this.clientWidth / 2; if (!this.isImage) { threshold = Math.max(threshold, this.clientWidth - 400); } - ref = clientX <= threshold ? [clientX + 45 + 'px', ''] : ['', this.clientWidth - clientX + 45 + 'px'], left = ref[0], right = ref[1]; + marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45; + if (this.isImage) { + marginX = Math.min(marginX, this.clientWidth - width); + } + marginX += 'px'; + ref1 = clientX <= threshold ? [marginX, ''] : ['', marginX], left = ref1[0], right = ref1[1]; style = this.style; style.top = top + 'px'; style.left = left; @@ -13879,8 +14001,11 @@ ImageCommon = (function() { return cb(URL); } }; - return $.ajax(location.protocol + "//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", { - onload: function() { + return $.ajax(Site.urls.threadJSON({ + boardID: post.boardID, + threadID: post.threadID + }), { + onloadend: function() { var i, len, postObj, ref; if (this.status === 404) { post.kill(!post.isClone); @@ -14454,7 +14579,7 @@ ImageHover = (function() { }, mouseover: function(post) { return function(e) { - var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x; + var el, error, file, height, isVideo, maxHeight, maxWidth, ref, ref1, scale, width, x; if (!doc.contains(this)) { return; } @@ -14490,28 +14615,32 @@ ImageHover = (function() { } } } - ref1 = (function() { - var i, len, ref1, results; - ref1 = file.dimensions.split('x'); - results = []; - for (i = 0, len = ref1.length; i < len; i++) { - x = ref1[i]; - results.push(+x); - } - return results; - })(), width = ref1[0], height = ref1[1]; - ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right; - maxWidth = Math.max(left, doc.clientWidth - right); - maxHeight = doc.clientHeight - UI.hover.padding; - scale = Math.min(1, maxWidth / width, maxHeight / height); - el.style.maxWidth = (scale * width) + "px"; - el.style.maxHeight = (scale * height) + "px"; + if (file.dimensions) { + ref1 = (function() { + var i, len, ref1, results; + ref1 = file.dimensions.split('x'); + results = []; + for (i = 0, len = ref1.length; i < len; i++) { + x = ref1[i]; + results.push(+x); + } + return results; + })(), width = ref1[0], height = ref1[1]; + maxWidth = doc.clientWidth; + maxHeight = doc.clientHeight - UI.hover.padding; + scale = Math.min(1, maxWidth / width, maxHeight / height); + width *= scale; + height *= scale; + el.style.maxWidth = width + "px"; + el.style.maxHeight = height + "px"; + } return UI.hover({ root: this, el: el, latestEvent: e, endEvents: 'mouseout click', - height: scale * height, + height: height, + width: width, noRemove: true, cb: function() { $.off(el, 'error', error); @@ -15369,7 +15498,7 @@ Embedding = (function() { return Embedding.flushTitles(service); } } else { - return CrossOrigin.json(service.api(uid), (function() { + return CrossOrigin.cache(service.api(uid), (function() { return Embedding.cb.title(this, data); })); } @@ -15388,7 +15517,7 @@ Embedding = (function() { Embedding.cb.title(this, data); } }; - return CrossOrigin.json(service.api((function() { + return CrossOrigin.cache(service.api((function() { var j, len, results; results = []; for (j = 0, len = queue.length; j < len; j++) { @@ -15610,7 +15739,7 @@ Embedding = (function() { hidden: true, id: "gist-embed-" + (counter++) }); - CrossOrigin.json("https://api.github.com/gists/" + a.dataset.uid, function() { + CrossOrigin.cache("https://api.github.com/gists/" + a.dataset.uid, function() { el.textContent = Object.values(this.response.files)[0].content; el.className = 'prettyprint'; $.global(function() { @@ -16341,18 +16470,21 @@ DeleteLink = (function() { return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), { responseType: 'document', withCredentials: true, - onload: function() { + onloadend: function() { return DeleteLink.load(link, post, fileOnly, this.response); }, - onerror: function() { - return DeleteLink.error(link, post); - } - }, { form: $.formData(form) }); }, load: function(link, post, fileOnly, resDoc) { var el, msg; + if (!resDoc) { + new Notice('warning', 'Connection error, please retry.', 20); + if (post.fullID === DeleteLink.post.fullID) { + $.on(link, 'click', DeleteLink.toggle); + } + return; + } link.textContent = DeleteLink.linkText(fileOnly); if (resDoc.title === '4chan - Banned') { el = $.el('span', { @@ -16381,12 +16513,6 @@ DeleteLink = (function() { } } }, - error: function(link, post) { - new Notice('warning', 'Connection error, please retry.', 20); - if (post.fullID === DeleteLink.post.fullID) { - return $.on(link, 'click', DeleteLink.toggle); - } - }, cooldown: { seconds: {}, start: function(post, seconds) { @@ -16986,7 +17112,10 @@ ExpandComment = (function() { return; } a.textContent = "Post No." + post + " Loading..."; - return $.cache(location.protocol + "//a.4cdn.org" + (a.pathname.split(/\/+/).splice(0, 4).join('/')) + ".json", function() { + return $.cache(Site.urls.threadJSON({ + boardID: post.boardID, + threadID: post.threadID + }), function() { return ExpandComment.parse(this, a, post); }); }, @@ -17004,7 +17133,7 @@ ExpandComment = (function() { var callback, clone, comment, href, i, j, k, len, len1, len2, postObj, posts, quote, ref, ref1, spoilerRange, status; status = req.status; if (status !== 200 && status !== 304) { - a.textContent = "Error " + req.statusText + " (" + status + ")"; + a.textContent = status ? "Error " + req.statusText + " (" + status + ")" : 'Connection Error'; return; } posts = req.response.posts; @@ -17085,15 +17214,16 @@ ExpandThread = (function() { return $.on(a, 'click', ExpandThread.cbToggle); }, disconnect: function(refresh) { - var ref, ref1, status, threadID; + var oldReq, ref, status, threadID; if (g.VIEW === 'thread' || !Conf['Thread Expansion']) { return; } ref = ExpandThread.statuses; for (threadID in ref) { status = ref[threadID]; - if ((ref1 = status.req) != null) { - ref1.abort(); + if ((oldReq = status.req)) { + delete status.req; + oldReq.abort(); } delete ExpandThread.statuses[threadID]; } @@ -17141,17 +17271,24 @@ ExpandThread = (function() { var status; ExpandThread.statuses[thread] = status = {}; a.textContent = Build.summaryText.apply(Build, ['...'].concat(slice.call(a.textContent.match(/\d+/g)))); - return status.req = $.cache(location.protocol + "//a.4cdn.org/" + thread.board + "/thread/" + thread + ".json", function() { + return status.req = $.cache(Site.urls.threadJSON({ + boardID: thread.board.ID, + threadID: thread.ID + }), function() { + if (this !== status.req) { + return; + } delete status.req; return ExpandThread.parse(this, thread, a); }); }, contract: function(thread, a, threadRoot) { - var filesCount, i, inlined, len, num, postsCount, replies, reply, status; + var filesCount, i, inlined, len, num, oldReq, postsCount, replies, reply, status; status = ExpandThread.statuses[thread]; delete ExpandThread.statuses[thread]; - if (status.req) { - status.req.abort(); + if ((oldReq = status.req)) { + delete status.req; + oldReq.abort(); if (a) { a.textContent = Build.summaryText.apply(Build, ['+'].concat(slice.call(a.textContent.match(/\d+/g)))); } @@ -17201,7 +17338,7 @@ ExpandThread = (function() { parse: function(req, thread, a) { var a2, filesCount, i, len, post, postData, posts, postsCount, postsRoot, ref, ref1, root; if ((ref = req.status) !== 200 && ref !== 304) { - a.textContent = "Error " + req.statusText + " (" + req.status + ")"; + a.textContent = req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error'; return; } Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler; @@ -18736,7 +18873,6 @@ Report = (function() { results = []; fn = function(name, url) { return $.ajax(url, { - responseType: 'json', onloadend: function() { results.push([ name, this.response || { @@ -18746,8 +18882,7 @@ Report = (function() { if (results.length === urls.length) { return cb(results); } - } - }, { + }, form: form }); }; @@ -19379,12 +19514,9 @@ ThreadStats = (function() { return; } ThreadStats.timeout = setTimeout(ThreadStats.fetchPage, 2 * $.MINUTE); - return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadStats.thread.board + "/threads.json", { - onload: ThreadStats.onThreadsLoad - }, { - whenModified: 'ThreadStats', - bypassCache: true - }); + return $.whenModified(Site.urls.threadsListJSON({ + boardID: ThreadStats.thread.board + }), 'ThreadStats', ThreadStats.onThreadsLoad); }, onThreadsLoad: function() { var i, j, k, len, len1, len2, page, purgePos, ref, ref1, ref2, thread; @@ -19593,11 +19725,12 @@ ThreadUpdater = (function() { } }, load: function() { - var req; - req = ThreadUpdater.req; - switch (req.status) { + if (this !== ThreadUpdater.req) { + return; + } + switch (this.status) { case 200: - ThreadUpdater.parse(req); + ThreadUpdater.parse(this); if (ThreadUpdater.thread.isArchived) { return ThreadUpdater.kill(); } else { @@ -19605,7 +19738,9 @@ ThreadUpdater = (function() { } break; case 404: - return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", { + return $.ajax(Site.urls.catalogJSON({ + boardID: ThreadUpdater.thread.board.ID + }), { onloadend: function() { var confirmed, i, k, len, len1, page, ref, ref1, thread; if (this.status === 200) { @@ -19628,12 +19763,12 @@ ThreadUpdater = (function() { if (confirmed) { return ThreadUpdater.kill(); } else { - return ThreadUpdater.error(req); + return ThreadUpdater.error(this); } } }); default: - return ThreadUpdater.error(req); + return ThreadUpdater.error(this); } } }, @@ -19711,18 +19846,18 @@ ThreadUpdater = (function() { return ThreadUpdater.seconds--; }, update: function() { - var ref; + var oldReq; clearTimeout(ThreadUpdater.timeoutID); ThreadUpdater.set('timer', '...', 'loading'); - if ((ref = ThreadUpdater.req) != null) { - ref.abort(); + if ((oldReq = ThreadUpdater.req)) { + delete ThreadUpdater.req; + oldReq.abort(); } - return ThreadUpdater.req = $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/thread/" + ThreadUpdater.thread + ".json", { - onloadend: ThreadUpdater.cb.load, + return ThreadUpdater.req = $.whenModified(Site.urls.threadJSON({ + boardID: ThreadUpdater.thread.board.ID, + threadID: ThreadUpdater.thread.ID + }), 'ThreadUpdater', ThreadUpdater.cb.load, { timeout: $.MINUTE - }, { - whenModified: 'ThreadUpdater', - bypassCache: true }); }, updateThreadStatus: function(type, status) { @@ -19882,6 +20017,7 @@ ThreadWatcher = (function() { className: 'fa fa-eye' }); this.db = new DataBoard('watchedThreads', this.refresh, true); + this.dbLM = new DataBoard('watcherLastModified', null, true); this.dialog = UI.dialog('thread-watcher', { innerHTML: "
Thread Watcher ×
" }); @@ -19912,6 +20048,7 @@ ThreadWatcher = (function() { this.dialog.hidden = true; } Header.addShortcut('watcher', sc, 510); + ThreadWatcher.initLastModified(); ThreadWatcher.fetchAuto(); $.on(window, 'visibilitychange focus', function() { return $.queueTask(ThreadWatcher.fetchAuto); @@ -20148,6 +20285,38 @@ ThreadWatcher = (function() { }, requests: [], fetched: 0, + fetch: function(url, arg, args, cb) { + var ajax, force, onloadend, ref, req, siteID; + siteID = arg.siteID, force = arg.force; + if (ThreadWatcher.requests.length === 0) { + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + } + onloadend = function() { + if (this.finished) { + return; + } + this.finished = true; + ThreadWatcher.fetched++; + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + ThreadWatcher.clearRequests(); + } else { + ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%"; + } + return cb.apply(this, args); + }; + ajax = siteID === Site.hostname ? $.ajax : CrossOrigin.ajax; + if (force) { + if ((ref = $.lastModified.ThreadWatcher) != null) { + delete ref[url]; + } + } + req = $.whenModified(url, 'ThreadWatcher', onloadend, { + timeout: $.MINUTE, + ajax: ajax + }); + return ThreadWatcher.requests.push(req); + }, clearRequests: function() { ThreadWatcher.requests = []; ThreadWatcher.fetched = 0; @@ -20156,15 +20325,44 @@ ThreadWatcher = (function() { }, abort: function() { var i, len1, ref, req; + delete ThreadWatcher.syncing; ref = ThreadWatcher.requests; for (i = 0, len1 = ref.length; i < len1; i++) { req = ref[i]; - if (req.readyState !== 4) { - req.abort(); + if (!(!req.finished)) { + continue; } + req.finished = true; + req.abort(); } return ThreadWatcher.clearRequests(); }, + initLastModified: function() { + var base, boardID, boards, data, date, lm, ref, ref1, siteID, url; + lm = ((base = $.lastModified)['ThreadWatcher'] || (base['ThreadWatcher'] = {})); + ref = ThreadWatcher.dbLM.data; + for (siteID in ref) { + boards = ref[siteID]; + ref1 = boards.boards; + for (boardID in ref1) { + data = ref1[boardID]; + if (ThreadWatcher.db.get({ + siteID: siteID, + boardID: boardID + })) { + for (url in data) { + date = data[url]; + lm[url] = date; + } + } else { + ThreadWatcher.dbLM["delete"]({ + siteID: siteID, + boardID: boardID + }); + } + } + } + }, fetchAuto: function() { var db, interval, now, ref; clearTimeout(ThreadWatcher.timeout); @@ -20176,41 +20374,123 @@ ThreadWatcher = (function() { now = Date.now(); if (!((now - interval < (ref = db.data.lastChecked || 0) && ref <= now) || d.hidden || !d.hasFocus())) { ThreadWatcher.fetchAllStatus(); - db.setLastChecked(); } return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval); }, buttonFetchAll: function() { - if (ThreadWatcher.requests.length) { + if (ThreadWatcher.syncing || ThreadWatcher.requests.length) { return ThreadWatcher.abort(); } else { return ThreadWatcher.fetchAllStatus(); } }, fetchAllStatus: function() { - var db, dbs, i, len1, n, results; + var dbi, dbs, i, len1, n, results; + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + ThreadWatcher.syncing = true; dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(function(x) { return x; }); n = 0; results = []; for (i = 0, len1 = dbs.length; i < len1; i++) { - db = dbs[i]; - results.push(db.forceSync(function() { - var j, len2, thread, threads; + dbi = dbs[i]; + results.push(dbi.forceSync(function() { + var board, boards, db, deep, j, len2, now, ref; if ((++n) === dbs.length) { - threads = ThreadWatcher.getAll(); - for (j = 0, len2 = threads.length; j < len2; j++) { - thread = threads[j]; - ThreadWatcher.fetchStatus(thread); + if (!ThreadWatcher.syncing) { + return; + } + delete ThreadWatcher.syncing; + db = ThreadWatcher.db; + now = Date.now(); + deep = !((now - 2 * $.HOUR < (ref = db.data.lastChecked2 || 0) && ref <= now)); + boards = ThreadWatcher.getAll(true); + for (j = 0, len2 = boards.length; j < len2; j++) { + board = boards[j]; + ThreadWatcher.fetchBoard(board, deep); + } + db.setLastChecked(); + if (deep) { + db.setLastChecked('lastChecked2'); + } + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + return ThreadWatcher.clearRequests(); } } })); } return results; }, + fetchBoard: function(board, deep) { + var base, boardID, ref, ref1, ref2, siteID, software, url, urlF; + if (!board.some(function(thread) { + return !thread.data.isDead; + })) { + return; + } + ref = board[0], siteID = ref.siteID, boardID = ref.boardID; + software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0; + urlF = deep && software === 'tinyboard' ? 'catalogJSON' : 'threadsListJSON'; + url = (ref2 = SW[software]) != null ? typeof (base = ref2.urls)[urlF] === "function" ? base[urlF]({ + siteID: siteID, + boardID: boardID + }) : void 0 : void 0; + if (!url) { + return; + } + return ThreadWatcher.fetch(url, { + siteID: siteID + }, [board, url], ThreadWatcher.parseBoard); + }, + parseBoard: function(board, url) { + var boardID, data, i, item, j, k, len1, len2, len3, lmDate, modified, page, ref, ref1, ref2, replies, siteID, thread, threadID; + if (this.status !== 200) { + return; + } + ref = board[0], siteID = ref.siteID, boardID = ref.boardID; + lmDate = this.getResponseHeader('Last-Modified'); + ThreadWatcher.dbLM.extend({ + siteID: siteID, + boardID: boardID, + val: $.item(url, lmDate) + }); + modified = {}; + replies = {}; + try { + ref1 = this.response; + for (i = 0, len1 = ref1.length; i < len1; i++) { + page = ref1[i]; + ref2 = page.threads; + for (j = 0, len2 = ref2.length; j < len2; j++) { + item = ref2[j]; + modified[item.no] = item.last_modified; + replies[item.no] = item.replies; + } + } + } catch (_error) {} + for (k = 0, len3 = board.length; k < len3; k++) { + thread = board[k]; + threadID = thread.threadID, data = thread.data; + if (modified[threadID]) { + if (modified[threadID] === data.modified && ((replies[threadID] == null) || replies[threadID] === data.replies)) { + continue; + } + ThreadWatcher.db.extend({ + siteID: siteID, + boardID: boardID, + threadID: threadID, + val: { + modified: modified[threadID] + } + }); + } + ThreadWatcher.fetchStatus(thread); + } + }, fetchStatus: function(thread, force) { - var base, boardID, data, ref, ref1, req, siteID, software, threadID, url; + var base, boardID, data, ref, ref1, siteID, software, threadID, url; siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data; software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0; url = (ref1 = SW[software]) != null ? typeof (base = ref1.urls).threadJSON === "function" ? base.threadJSON({ @@ -20227,46 +20507,18 @@ ThreadWatcher = (function() { if (data.last === -1) { return; } - if (ThreadWatcher.requests.length === 0) { - ThreadWatcher.status.textContent = '...'; - $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); - } - if ((typeof Site.hasCORS === "function" ? Site.hasCORS(url) : void 0) || url.split('/').slice(0, 3).join('/') === location.origin) { - req = $.ajax(url, { - onloadend: function() { - return ThreadWatcher.parseStatus.call(this, thread); - }, - timeout: $.MINUTE - }, { - whenModified: force ? false : 'ThreadWatcher' - }); - } else { - req = { - abort: function() { - return req.aborted = true; - } - }; - CrossOrigin.json(url, function() { - if (req.aborted) { - return; - } - return ThreadWatcher.parseStatus.call(this, thread); - }, true, $.MINUTE); - } - return ThreadWatcher.requests.push(req); + return ThreadWatcher.fetch(url, { + siteID: siteID, + force: force + }, [thread], ThreadWatcher.parseStatus); }, parseStatus: function(arg) { - var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, siteID, software, threadID, unread, updated, youOP; + var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, replies, siteID, software, threadID, unread, updated, youOP; siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, data = arg.data; - ThreadWatcher.fetched++; - if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { - ThreadWatcher.clearRequests(); - } else { - ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%"; - } software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0; if (this.status === 200 && this.response) { last = this.response.posts[this.response.posts.length - 1].no; + replies = this.response.posts.length - 1; isDead = !!this.response.posts[0].archived; if (isDead && Conf['Auto Prune']) { ThreadWatcher.db["delete"]({ @@ -20341,6 +20593,7 @@ ThreadWatcher = (function() { threadID: threadID, val: { last: last, + replies: replies, isDead: isDead, unread: unread, quotingYou: quotingYou @@ -20380,8 +20633,8 @@ ThreadWatcher = (function() { return ThreadWatcher.refresh(); } }, - getAll: function() { - var all, boardID, boards, data, ref, ref1, siteID, threadID, threads; + getAll: function(groupByBoard) { + var all, boardID, boards, cont, data, ref, ref1, siteID, threadID, threads; all = []; ref = ThreadWatcher.db.data; for (siteID in ref) { @@ -20392,10 +20645,13 @@ ThreadWatcher = (function() { if (Conf['Current Board'] && (siteID !== Site.hostname || boardID !== g.BOARD.ID)) { continue; } + if (groupByBoard) { + all.push((cont = [])); + } for (threadID in threads) { data = threads[threadID]; if (data && typeof data === 'object') { - all.push({ + (groupByBoard ? cont : all).push({ siteID: siteID, boardID: boardID, threadID: threadID, @@ -20949,12 +21205,15 @@ Unread = (function() { return; } }, - openNotification: function(post) { + openNotification: function(post, predicate) { var notif; + if (predicate == null) { + predicate = ' replied to you'; + } if (!Header.areNotificationsEnabled) { return; } - notif = new Notification(post.info.nameBlock + " replied to you", { + notif = new Notification("" + post.info.nameBlock + predicate, { body: post.commentDisplay(), icon: Favicon.logo }); @@ -22730,7 +22989,7 @@ QR = (function() { } }, submit: function(e) { - var captcha, cb, err, extra, filetag, formData, options, post, ref, thread, threadID; + var captcha, cb, err, filetag, formData, options, post, ref, thread, threadID; if (e != null) { e.preventDefault(); } @@ -22800,47 +23059,35 @@ QR = (function() { options = { responseType: 'document', withCredentials: true, - onload: QR.response, - onerror: function() { - delete QR.req; - if (QR.currentCaptcha) { - Captcha.cache.save(QR.currentCaptcha); - } - delete QR.currentCaptcha; - post.unlock(); - QR.cooldown.auto = true; - QR.cooldown.addDelay(post, 2); - QR.status(); - return QR.error(QR.connectionError()); - } - }; - extra = { + onloadend: QR.response, form: $.formData(formData) }; if (Conf['Show Upload Progress']) { - extra.upCallbacks = { - onload: function() { + options.onprogress = function(e) { + var ref1; + if (this !== ((ref1 = QR.req) != null ? ref1.upload : void 0)) { + return; + } + if (e.loaded < e.total) { + QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%"; + } else { QR.req.isUploadFinished = true; QR.req.progress = '...'; - return QR.status(); - }, - onprogress: function(e) { - QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%"; - return QR.status(); } + return QR.status(); }; } cb = function(response) { if (response != null) { QR.currentCaptcha = response; if (response.challenge != null) { - extra.form.append('recaptcha_challenge_field', response.challenge); - extra.form.append('recaptcha_response_field', response.response); + options.form.append('recaptcha_challenge_field', response.challenge); + options.form.append('recaptcha_response_field', response.response); } else { - extra.form.append('g-recaptcha-response', response.response); + options.form.append('g-recaptcha-response', response.response); } } - QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options, extra); + QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options); return QR.req.progress = '...'; }; if (typeof captcha === 'function') { @@ -22866,23 +23113,24 @@ QR = (function() { return QR.status(); }, response: function() { - var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, req, resDoc, seconds, threadID; - req = QR.req; + var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, ref3, seconds, threadID; + if (this !== QR.req) { + return; + } delete QR.req; post = QR.posts[0]; post.unlock(); - resDoc = req.response; - if ((err = resDoc.getElementById('errmsg'))) { - if ((ref = $('a', err)) != null) { - ref.target = '_blank'; + if ((err = (ref = this.response) != null ? ref.getElementById('errmsg') : void 0)) { + if ((ref1 = $('a', err)) != null) { + ref1.target = '_blank'; } - } else if ((connErr = resDoc.title !== 'Post successful!')) { + } else if ((connErr = !this.response || this.response.title !== 'Post successful!')) { err = QR.connectionError(); if (QR.currentCaptcha) { Captcha.cache.save(QR.currentCaptcha); } - } else if (req.status !== 200) { - err = "Error " + req.statusText + " (" + req.status + ")"; + } else if (this.status !== 200) { + err = "Error " + this.statusText + " (" + this.status + ")"; } delete QR.currentCaptcha; if (err) { @@ -22905,13 +23153,13 @@ QR = (function() { } else { QR.cooldown.auto = false; } - QR.captcha.setup(QR.cooldown.auto && ((ref1 = d.activeElement) === QR.nodes.status || ref1 === d.body)); + QR.captcha.setup(QR.cooldown.auto && ((ref2 = d.activeElement) === QR.nodes.status || ref2 === d.body)); QR.status(); QR.error(err); return; } - h1 = $('h1', resDoc); - ref2 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref2[0], threadID = ref2[1], postID = ref2[2]; + h1 = $('h1', this.response); + ref3 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref3[0], threadID = ref3[1], postID = ref3[2]; postID = +postID; threadID = +threadID || postID; isReply = threadID !== postID; @@ -22928,10 +23176,10 @@ QR = (function() { postsCount = QR.posts.length - 1; QR.cooldown.auto = postsCount && isReply; lastPostToThread = !((function() { - var j, len, p, ref3; - ref3 = QR.posts.slice(1); - for (j = 0, len = ref3.length; j < len; j++) { - p = ref3[j]; + var j, len, p, ref4; + ref4 = QR.posts.slice(1); + for (j = 0, len = ref4.length; j < len; j++) { + p = ref4[j]; if (p.thread === post.thread) { return true; } @@ -22982,17 +23230,18 @@ QR = (function() { } else { return setTimeout(check, attempts * $.SECOND); } - } - }, { + }, + responseType: 'text', type: 'HEAD' }); }; return check(); }, abort: function() { - if (QR.req && !QR.req.isUploadFinished) { - QR.req.abort(); + var oldReq; + if ((oldReq = QR.req) && !QR.req.isUploadFinished) { delete QR.req; + oldReq.abort(); if (QR.currentCaptcha) { Captcha.cache.save(QR.currentCaptcha); } diff --git a/builds/4chan-X-noupdate.crx b/builds/4chan-X-noupdate.crx index 4d042d99f..fde638f8d 100644 Binary files a/builds/4chan-X-noupdate.crx and b/builds/4chan-X-noupdate.crx differ diff --git a/builds/4chan-X-noupdate.user.js b/builds/4chan-X-noupdate.user.js index f7015f2ea..3d63ff109 100644 --- a/builds/4chan-X-noupdate.user.js +++ b/builds/4chan-X-noupdate.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X -// @version 1.14.5.14 +// @version 1.14.6.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X @@ -198,7 +198,7 @@ docSet = function() { }; g = { - VERSION: '1.14.5.14', + VERSION: '1.14.6.0', NAMESPACE: '4chan X.', boards: {} }; @@ -241,6 +241,7 @@ Config = (function() { 'JSON Index': [true, 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'], 'Use 4chan X Catalog': [true, 'Link to 4chan X\'s catalog instead of the native 4chan one.', 1], 'Index Refresh Notifications': [false, 'Show a notice at the top of the page when the index is refreshed.', 1], + 'Follow Cursor': [true, 'Image Hover and Quote Preview move with the mouse cursor.'], 'Open Threads in New Tab': [false, 'Make links to threads in the index / 4chan X catalog open in a new tab.'], 'External Catalog': [false, 'Link to external catalog instead of the internal one.'], 'Catalog Links': [false, 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'], @@ -1390,6 +1391,10 @@ body.is_catalog .thread > a > img {\n\ .nwsb {\n\ display: inline;\n\ }\n\ +.fileText {\n\ + max-width: auto;\n\ + white-space: normal;\n\ +}\n\ /* Ads */\n\ .ad-cnt > *, .adg-rects > *, .bsa-cnt {\n\ height: auto !important;\n\ @@ -2587,6 +2592,13 @@ span.hide-announcement {\n\ .fileThumb > .warning {\n\ clear: both;\n\ }\n\ +#ihover {\n\ + pointer-events: none;\n\ + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */\n\ + max-height: 95vh;\n\ + max-height: calc(100vh - 25px);\n\ + max-width: 100vw;\n\ +}\n\ /* WEBM Metadata */\n\ .webm-title > a::before {\n\ content: \"title\";\n\ @@ -4595,58 +4607,43 @@ $ = (function() { }; $.ajax = (function() { - var lastModified, pageXHR; - lastModified = {}; + var pageXHR; if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) { pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest); } else { pageXHR = XMLHttpRequest; } - return function(url, options, extra) { - var bypassCache, err, event, form, j, len, params, r, ref, ref1, type, upCallbacks, url0, whenModified; + return function(url, options) { + var err, form, headers, key, onloadend, onprogress, r, ref, responseType, timeout, type, value, withCredentials; if (options == null) { options = {}; } - if (extra == null) { - extra = {}; - } - type = extra.type, whenModified = extra.whenModified, bypassCache = extra.bypassCache, upCallbacks = extra.upCallbacks, form = extra.form; - if (/\.json$/.test(url)) { - if (options.responseType == null) { - options.responseType = 'json'; - } + onloadend = options.onloadend, timeout = options.timeout, responseType = options.responseType, withCredentials = options.withCredentials, type = options.type, onprogress = options.onprogress, form = options.form, headers = options.headers; + if (responseType == null) { + responseType = 'json'; } url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'); - if (whenModified) { - params = []; - if ($.engine === 'blink') { - params.push("s=" + whenModified); - } - if (Site.software === 'yotsuba' && bypassCache) { - params.push("t=" + (Date.now())); - } - url0 = url; - if (params.length) { - url += '?' + params.join('&'); - } - } r = new pageXHR(); type || (type = form && 'post' || 'get'); try { r.open(type, url, true); - if (whenModified) { - if (((ref = lastModified[whenModified]) != null ? ref[url0] : void 0) != null) { - r.setRequestHeader('If-Modified-Since', lastModified[whenModified][url0]); - } - $.on(r, 'load', function() { - return (lastModified[whenModified] || (lastModified[whenModified] = {}))[url0] = r.getResponseHeader('Last-Modified'); - }); + ref = headers || {}; + for (key in ref) { + value = ref[key]; + r.setRequestHeader(key, value); } - $.extend(r, options); - $.extend(r.upload, upCallbacks); + $.extend(r, { + onloadend: onloadend, + timeout: timeout, + responseType: responseType, + withCredentials: withCredentials + }); + $.extend(r.upload, { + onprogress: onprogress + }); $.on(r, 'error', function() { if (!r.status) { - return c.error("4chan X failed to load: " + url); + return c.warn("4chan X failed to load: " + url); } }); r.send(form); @@ -4655,51 +4652,82 @@ $ = (function() { if (err.result !== 0x805e0006) { throw err; } - ref1 = ['error', 'loadend']; - for (j = 0, len = ref1.length; j < len; j++) { - event = ref1[j]; - r["on" + event] = options["on" + event]; - $.queueTask($.event, event, null, r); - } + r.onloadend = onloadend; + $.queueTask($.event, 'error', null, r); + $.queueTask($.event, 'loadend', null, r); } return r; }; })(); + $.lastModified = {}; + + $.whenModified = function(url, bucket, cb, options) { + var ajax, headers, params, r, ref, t, timeout, url0; + if (options == null) { + options = {}; + } + timeout = options.timeout, ajax = options.ajax; + params = []; + if ($.engine === 'blink') { + params.push("s=" + bucket); + } + if (Site.software === 'yotsuba') { + params.push("t=" + (Date.now())); + } + url0 = url; + if (params.length) { + url += '?' + params.join('&'); + } + headers = {}; + if ((t = (ref = $.lastModified[bucket]) != null ? ref[url0] : void 0) != null) { + headers['If-Modified-Since'] = t; + } + r = (ajax || $.ajax)(url, { + onloadend: function() { + var base; + ((base = $.lastModified)[bucket] || (base[bucket] = {}))[url0] = this.getResponseHeader('Last-Modified'); + return cb.call(this); + }, + timeout: timeout, + headers: headers + }); + return r; + }; + (function() { var reqs; reqs = {}; $.cache = function(url, cb, options) { - var err, req, rm; - if (req = reqs[url]) { - if (req.readyState === 4) { - $.queueTask(function() { - return cb.call(req, req.evt, true); - }); - } else { + var ajax, onloadend, req; + if (options == null) { + options = {}; + } + ajax = options.ajax; + if ((req = reqs[url])) { + if (req.callbacks) { req.callbacks.push(cb); + } else { + $.queueTask(function() { + return cb.call(req, { + isCached: true + }); + }); } return req; } - rm = function() { - return delete reqs[url]; - }; - try { - if (!(req = $.ajax(url, options))) { - return; - } - } catch (_error) { - err = _error; - return; - } - $.on(req, 'load', function(e) { + onloadend = function() { var fn1, j, len, ref; - this.evt = e; + if (!this.status) { + delete reqs[url]; + } ref = this.callbacks; fn1 = (function(_this) { return function(cb) { return $.queueTask(function() { - return cb.call(_this, e, false); + return cb.call(_this, { + isCached: false + }); }); }; })(this); @@ -4708,8 +4736,10 @@ $ = (function() { fn1(cb); } return delete this.callbacks; + }; + req = (ajax || $.ajax)(url, { + onloadend: onloadend }); - $.on(req, 'abort error', rm); req.callbacks = [cb]; return reqs[url] = req; }; @@ -5402,7 +5432,7 @@ $$ = (function() { }).call(this); CrossOrigin = (function() { - var CrossOrigin; + var CrossOrigin, Request; CrossOrigin = { binary: function(url, cb, headers) { @@ -5471,95 +5501,94 @@ CrossOrigin = (function() { return cb(blob); }); }, - json: (function() { - var callbacks, failure, results, success; - callbacks = {}; - results = {}; - success = function(url, result) { - var cb, j, len, ref; - ref = callbacks[url]; - for (j = 0, len = ref.length; j < len; j++) { - cb = ref[j]; - $.queueTask(function() { - return cb.call(result); - }); - } - delete callbacks[url]; - return results[url] = result; - }; - failure = function(url) { - var cb, j, len, ref; - ref = callbacks[url]; - for (j = 0, len = ref.length; j < len; j++) { - cb = ref[j]; - $.queueTask(function() { - return cb.call({}); - }); - } - return delete callbacks[url]; - }; - return function(url, cb, bypassCache, timeout) { - var req; - if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) { - if (bypassCache) { - $.cleanCache(function(url2) { - return url2 === url; - }); - } - if ((req = $.cache(url, cb, { - responseType: 'json' - }))) { - $.on(req, 'abort error', function() { - return cb.call({}); - }); - } else { - cb.call({}); - } - return; - } - if (bypassCache) { - delete results[url]; - } else { - if (results[url]) { - cb.call(results[url]); - return; - } - if (callbacks[url]) { - callbacks[url].push(cb); - return; - } - } - callbacks[url] = [cb]; - return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({ - method: "GET", - url: url + '', - timeout: timeout, - onload: function(xhr) { - var response, status, statusText; - status = xhr.status, statusText = xhr.statusText; - try { - response = JSON.parse(xhr.responseText); - return success(url, { - status: status, - statusText: statusText, - response: response - }); - } catch (_error) { - return failure(url); + Request: Request = (function() { + function Request() {} + + Request.prototype.status = 0; + + Request.prototype.statusText = ''; + + Request.prototype.response = null; + + Request.prototype.responseHeaderString = null; + + Request.prototype.getResponseHeader = function(headerName) { + var header, i, j, key, len, ref, ref1, val; + if ((this.responseHeaders == null) && (this.responseHeaderString != null)) { + this.responseHeaders = {}; + ref = this.responseHeaderString.split('\r\n'); + for (j = 0, len = ref.length; j < len; j++) { + header = ref[j]; + if ((i = header.indexOf(':')) >= 0) { + key = header.slice(0, i).trim().toLowerCase(); + val = header.slice(i + 1).trim(); + this.responseHeaders[key] = val; } - }, - onerror: function() { - return failure(url); - }, - onabort: function() { - return failure(url); - }, - ontimeout: function() { - return failure(url); } - }); + } + return (ref1 = (this.responseHeaders || {})[headerName.toLowerCase()]) != null ? ref1 : null; }; + + Request.prototype.abort = function() {}; + + Request.prototype.onloadend = function() {}; + + return Request; + })(), + ajax: function(url, options) { + var gmReq, headers, onloadend, req, timeout; + if (options == null) { + options = {}; + } + onloadend = options.onloadend, timeout = options.timeout, headers = options.headers; + if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) { + return $.ajax(url, options); + } + req = new CrossOrigin.Request(); + req.onloadend = onloadend; + gmReq = ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({ + method: 'GET', + url: url, + headers: headers, + timeout: timeout, + onload: function(xhr) { + var response; + try { + response = xhr.responseText ? JSON.parse(xhr.responseText) : null; + $.extend(req, { + response: response, + status: xhr.status, + statusText: xhr.statusText, + responseHeaderString: xhr.responseHeaders + }); + } catch (_error) {} + return req.onloadend(); + }, + onerror: function() { + return req.onloadend(); + }, + onabort: function() { + return req.onloadend(); + }, + ontimeout: function() { + return req.onloadend(); + } + }); + if (typeof gmReq.abort === 'function') { + req.abort = function() { + try { + return gmReq.abort(); + } catch (_error) {} + }; + } + return req; + }, + cache: function(url, cb) { + return $.cache(url, cb, { + ajax: CrossOrigin.ajax + }); + }, permission: function(cb) { return cb(); } @@ -5764,7 +5793,7 @@ DataBoard = (function() { bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; DataBoard = (function() { - DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']; + DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']; function DataBoard(key1, sync, dontClean) { var init; @@ -5952,7 +5981,7 @@ DataBoard = (function() { boardID: boardID, threadID: threadID, postID: postID, - val: {} + defaultValue: {} }); ref = rm || []; for (i = 0, len = ref.length; i < len; i++) { @@ -5971,10 +6000,13 @@ DataBoard = (function() { })(this), cb); }; - DataBoard.prototype.setLastChecked = function() { + DataBoard.prototype.setLastChecked = function(key) { + if (key == null) { + key = 'lastChecked'; + } return this.save((function(_this) { return function() { - return _this.data.lastChecked = Date.now(); + return _this.data[key] = Date.now(); }; })(this)); }; @@ -6005,9 +6037,6 @@ DataBoard = (function() { DataBoard.prototype.clean = function() { var boardID, now, ref, ref1, siteID, val; - if (Site.software !== 'yotsuba') { - return; - } siteID = Site.hostname; ref = this.data[siteID].boards; for (boardID in ref) { @@ -6027,21 +6056,36 @@ DataBoard = (function() { }; DataBoard.prototype.ajaxClean = function(boardID) { - return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/threads.json", (function(_this) { - return function(e1) { - var response1; - if (e1.target.status !== 200) { + var base, siteID, that, threadsList; + that = this; + siteID = Site.hostname; + threadsList = typeof (base = Site.urls).threadsListJSON === "function" ? base.threadsListJSON({ + siteID: siteID, + boardID: boardID + }) : void 0; + if (!threadsList) { + return; + } + return $.cache(threadsList, function() { + var archiveList, base1, response1; + if (this.status !== 200) { + return; + } + archiveList = typeof (base1 = Site.urls).archiveListJSON === "function" ? base1.archiveListJSON({ + siteID: siteID, + boardID: boardID + }) : void 0; + if (!archiveList) { + return that.ajaxCleanParse(boardID, this.response); + } + response1 = this.response; + return $.cache(archiveList, function() { + if (this.status !== 200) { return; } - response1 = e1.target.response; - return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/archive.json", function(e2) { - if (!(e2.target.status === 200 || (boardID === 'b' || boardID === 'f' || boardID === 'trash' || boardID === 'bant'))) { - return; - } - return _this.ajaxCleanParse(boardID, response1, e2.target.response); - }); - }; - })(this)); + return that.ajaxCleanParse(boardID, response1, this.response); + }); + }); }; DataBoard.prototype.ajaxCleanParse = function(boardID, response1, response2) { @@ -6102,7 +6146,7 @@ Fetcher = (function() { Fetcher = (function() { function Fetcher(boardID1, threadID, postID1, root, quoter) { - var board, post, ref, thread; + var board, post, ref, that, thread; this.boardID = boardID1; this.threadID = threadID; this.postID = postID1; @@ -6122,11 +6166,15 @@ Fetcher = (function() { } this.root.textContent = "Loading post No." + this.postID + "..."; if (this.threadID) { - $.cache(location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json", (function(_this) { - return function(e, isCached) { - return _this.fetchedPost(e.target, isCached); - }; - })(this)); + that = this; + $.cache(Site.urls.threadJSON({ + boardID: this.boardID, + threadID: this.threadID + }), function(arg) { + var isCached; + isCached = arg.isCached; + return that.fetchedPost(this, isCached); + }); } else { this.archivedPost(); } @@ -6165,18 +6213,18 @@ Fetcher = (function() { }; Fetcher.prototype.fetchedPost = function(req, isCached) { - var api, board, k, len, post, posts, status, thread; + var api, board, k, len, post, posts, status, that, thread; if (post = g.posts[this.boardID + "." + this.postID]) { this.insert(post); return; } status = req.status; if (status !== 200) { - if (this.archivedPost()) { + if (status && this.archivedPost()) { return; } $.addClass(this.root, 'warning'); - this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : "Error " + req.statusText + " (" + req.status + ")."; + this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : !status ? 'Connection Error' : "Error " + req.statusText + " (" + req.status + ")."; return; } posts = req.response.posts; @@ -6189,15 +6237,17 @@ Fetcher = (function() { } if (post.no !== this.postID) { if (isCached) { - api = location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json"; + api = Site.urls.threadJSON({ + boardID: this.boardID, + threadID: this.threadID + }); $.cleanCache(function(url) { return url === api; }); - $.cache(api, (function(_this) { - return function(e) { - return _this.fetchedPost(e.target, false); - }; - })(this)); + that = this; + $.cache(api, function() { + return that.fetchedPost(this, false); + }); return; } if (this.archivedPost()) { @@ -6230,7 +6280,7 @@ Fetcher = (function() { encryptionOK = /^https:\/\//.test(url) || location.protocol === 'http:'; if (encryptionOK || Conf['Exempt Archives from Encryption']) { that = this; - CrossOrigin.json(url, function() { + CrossOrigin.cache(url, function() { var key, media, ref, ref1; if (!encryptionOK && ((ref = this.response) != null ? ref.media : void 0)) { media = this.response.media; @@ -7359,6 +7409,26 @@ SW = {}; } else { return ''; } + }, + threadsListJSON: function(arg) { + var boardID, ref, root, siteID; + siteID = arg.siteID, boardID = arg.boardID; + root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0; + if (root) { + return "" + root + boardID + "/threads.json"; + } else { + return ''; + } + }, + catalogJSON: function(arg) { + var boardID, ref, root, siteID; + siteID = arg.siteID, boardID = arg.boardID; + root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0; + if (root) { + return "" + root + boardID + "/catalog.json"; + } else { + return ''; + } } }, selectors: { @@ -7475,6 +7545,25 @@ SW = {}; var boardID, threadID; boardID = arg.boardID, threadID = arg.threadID; return location.protocol + "//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json"; + }, + threadsListJSON: function(arg) { + var boardID; + boardID = arg.boardID; + return location.protocol + "//a.4cdn.org/" + boardID + "/threads.json"; + }, + archiveListJSON: function(arg) { + var boardID; + boardID = arg.boardID; + if (BoardConfig.isArchived(boardID)) { + return location.protocol + "//a.4cdn.org/" + boardID + "/archive.json"; + } else { + return ''; + } + }, + catalogJSON: function(arg) { + var boardID; + boardID = arg.boardID; + return location.protocol + "//a.4cdn.org/" + boardID + "/catalog.json"; } }, selectors: { @@ -7565,7 +7654,10 @@ SW = {}; thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : void 0; if (g.BOARD.ID === 'f' && thread.OP.file) { file = thread.OP.file; - return $.ajax(location.protocol + "//a.4cdn.org/f/thread/" + thread + ".json", { + return $.ajax(Site.urls.threadJSON({ + boardID: 'f', + threadID: thread.ID + }), { timeout: $.MINUTE, onloadend: function() { if (this.response) { @@ -7860,7 +7952,9 @@ Redirect = (function() { response: response }); } else { - CrossOrigin.json(url, load(i), true); + CrossOrigin.ajax(url, { + onloadend: load(i) + }); } } } else { @@ -8025,7 +8119,7 @@ Filter = (function() { filters: {}, results: {}, init: function() { - var base, base1, boards, err, excludes, filter, hl, i, j, key, len, len1, line, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, sfwBoards, stub, top, type, types; + var base, base1, board, boards, boardsRaw, err, excludes, excludesRaw, file, filter, hide, hl, i, isstring, j, k, key, l, len, len1, len2, len3, line, mask, noti, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, regexp, sfwBoards, stub, top, type, types; if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) { return; } @@ -8041,16 +8135,33 @@ Filter = (function() { if (line[0] === '#') { continue; } - if (!(regexp = line.match(/\/(.+)\/(\w*)/))) { + if (!(regexp = line.match(/\/(.*)\/(\w*)/))) { continue; } filter = line.replace(regexp[0], ''); - boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global'; - boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards); - boards = boards === 'global' ? null : boards.split(','); - excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase() : void 0) || null; - excludes = excludes === null ? null : excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); - if (key === 'uniqueID' || key === 'MD5') { + boardsRaw = (ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0; + if (boardsRaw) { + boards = {}; + ref3 = boardsRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); + for (j = 0, len1 = ref3.length; j < len1; j++) { + board = ref3[j]; + boards[board] = true; + } + } else { + boards = false; + } + excludesRaw = (ref4 = filter.match(/exclude:([^;]+)/)) != null ? ref4[1].toLowerCase() : void 0; + if (excludesRaw) { + excludes = {}; + ref5 = excludesRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); + for (k = 0, len2 = ref5.length; k < len2; k++) { + board = ref5[k]; + excludes[board] = true; + } + } else { + excludes = false; + } + if ((isstring = (key === 'uniqueID' || key === 'MD5'))) { regexp = regexp[1]; } else { try { @@ -8061,10 +8172,19 @@ Filter = (function() { continue; } } - op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes'; + op = ((ref6 = filter.match(/[^t]op:(no|only)/)) != null ? ref6[1] : void 0) || ''; + mask = { + 'no': 1, + 'only': 2 + }[op] || 0; + file = ((ref7 = filter.match(/file:(no|only)/)) != null ? ref7[1] : void 0) || ''; + mask = mask | ({ + 'no': 4, + 'only': 8 + }[file] || 0); stub = (function() { - var ref5; - switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) { + var ref8; + switch ((ref8 = filter.match(/stub:(yes|no)/)) != null ? ref8[1] : void 0) { case 'yes': return true; case 'no': @@ -8073,9 +8193,10 @@ Filter = (function() { return Conf['Stubs']; } })(); - if (hl = /highlight/.test(filter)) { - hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight'; - top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes'; + noti = /notify/.test(filter); + if ((hl = /highlight/.test(filter))) { + hl = ((ref8 = filter.match(/highlight:([\w-]+)/)) != null ? ref8[1] : void 0) || 'filter-highlight'; + top = ((ref9 = filter.match(/top:(yes|no)/)) != null ? ref9[1] : void 0) || 'yes'; top = top === 'yes'; } if (key === 'general') { @@ -8087,10 +8208,22 @@ Filter = (function() { types = ['subject', 'name', 'filename', 'comment']; } } - filter = this.createFilter(regexp, boards, excludes, op, stub, hl, top); + hide = !(hl || noti); + filter = { + isstring: isstring, + regexp: regexp, + boards: boards, + excludes: excludes, + mask: mask, + hide: hide, + stub: stub, + hl: hl, + top: top, + noti: noti + }; if (key === 'general') { - for (j = 0, len1 = types.length; j < len1; j++) { - type = types[j]; + for (l = 0, len3 = types.length; l < len3; l++) { + type = types[l]; ((base = this.filters)[type] || (base[type] = [])).push(filter); } } else { @@ -8106,37 +8239,8 @@ Filter = (function() { cb: this.node }); }, - createFilter: function(regexp, boards, excludes, op, stub, hl, top) { - var settings, test; - test = typeof regexp === 'string' ? function(value) { - return regexp === value; - } : function(value) { - return regexp.test(value); - }; - settings = { - hide: !hl, - stub: stub, - "class": hl, - top: top - }; - return function(value, boardID, isReply) { - if (boards && indexOf.call(boards, boardID) < 0) { - return false; - } - if (excludes && indexOf.call(excludes, boardID) >= 0) { - return false; - } - if (isReply && op === 'only' || !isReply && op === 'no') { - return false; - } - if (!test(value)) { - return false; - } - return settings; - }; - }, test: function(post, hideable) { - var filter, hide, hl, i, key, len, ref, ref1, result, stub, top, value; + var filter, hide, hl, i, key, len, mask, noti, ref, ref1, stub, top, value; if (hideable == null) { hideable = true; } @@ -8147,25 +8251,32 @@ Filter = (function() { stub = true; hl = void 0; top = false; + noti = false; if (QuoteYou.isYou(post)) { hideable = false; } + mask = (post.isReply ? 2 : 1); + mask = mask | (post.file ? 4 : 8); for (key in Filter.filters) { if (((value = Filter[key](post)) != null)) { ref = Filter.filters[key]; for (i = 0, len = ref.length; i < len; i++) { filter = ref[i]; - if ((result = filter(value, post.boardID, post.isReply))) { - if (result.hide) { - if (hideable) { - hide = true; - stub && (stub = result.stub); - } - } else { - if (!(hl && (ref1 = result["class"], indexOf.call(hl, ref1) >= 0))) { - (hl || (hl = [])).push(result["class"]); - } - top || (top = result.top); + if ((filter.boards && !filter.boards[post.boardID]) || (filter.excludes && filter.excludes[post.boardID]) || (filter.mask & mask) || (filter.isstring ? filter.regexp !== value : !filter.regexp.test(value))) { + continue; + } + if (filter.hide) { + if (hideable) { + hide = true; + stub && (stub = filter.stub); + } + } else { + if (!(hl && (ref1 = filter.hl, indexOf.call(hl, ref1) >= 0))) { + (hl || (hl = [])).push(filter.hl); + } + top || (top = filter.top); + if (filter.noti) { + noti = true; } } } @@ -8179,16 +8290,17 @@ Filter = (function() { } else { return { hl: hl, - top: top + top: top, + noti: noti }; } }, node: function() { - var hide, hl, ref, stub, top; + var hide, hl, noti, ref, stub, top; if (this.isClone) { return; } - ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top; + ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top, noti = ref.noti; if (hide) { if (this.isReply) { PostHiding.hide(this, stub); @@ -8201,6 +8313,9 @@ Filter = (function() { $.addClass.apply($, [this.nodes.root].concat(slice.call(hl))); } } + if (noti && Unread.posts && (this.ID > Unread.lastReadPost) && !QuoteYou.isYou(this)) { + return Unread.openNotification(this, ' triggered a notification filter'); + } }, isHidden: function(post) { return !!Filter.test(post).hide; @@ -8212,7 +8327,7 @@ Filter = (function() { return post.info.name; }, uniqueID: function(post) { - return post.info.uniqueID; + return post.info.uniqueID || ''; }, tripcode: function(post) { return post.info.tripcode; @@ -9137,6 +9252,11 @@ BoardConfig = (function() { domain: function(board) { return "boards." + (BoardConfig.isSFW(board) ? '4channel' : '4chan') + ".org"; }, + isArchived: function(board) { + var data; + data = (this.boards || Conf['boardConfig'].boards)[board]; + return !data || data.is_archived; + }, noAudio: function(boardID) { var boards; if (Site.software !== 'yotsuba') { @@ -9904,7 +10024,7 @@ Header = (function() { } } if (/-expired/.test(t)) { - if (boardID !== 'b' && boardID !== 'f' && boardID !== 'trash' && boardID !== 'bant') { + if (BoardConfig.isArchived(boardID)) { a.href = "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/archive"; } else { return a.firstChild; @@ -10230,7 +10350,7 @@ Index = (function() { showHiddenThreads: false, changed: {}, init: function() { - var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, select, sortEntry, tRaw, watchSettings; + var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, select, sortEntry, tRaw, watchSettings; if (!(g.VIEW === 'index' && g.BOARD.ID !== 'f')) { return; } @@ -10320,7 +10440,7 @@ Index = (function() { innerHTML: "Index Catalog Archive Bottom ×" }); $('.cataloglink a', this.navLinks).href = CatalogLinks.catalog(); - if ((ref5 = g.BOARD.ID) === 'b' || ref5 === 'trash' || ref5 === 'bant') { + if (!BoardConfig.isArchived(g.BOARD.ID)) { $('.archlistlink', this.navLinks).hidden = true; } $.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront); @@ -10339,9 +10459,9 @@ Index = (function() { $.on(this.selectSort, 'change', this.cb.sort); $.on(this.selectSize, 'change', $.cb.value); $.on(this.selectSize, 'change', this.cb.size); - ref6 = [this.selectMode, this.selectSize]; - for (k = 0, len1 = ref6.length; k < len1; k++) { - select = ref6[k]; + ref5 = [this.selectMode, this.selectSize]; + for (k = 0, len1 = ref5.length; k < len1; k++) { + select = ref5[k]; select.value = Conf[select.name]; } this.selectRev.checked = /-rev$/.test(Index.currentSort); @@ -10350,12 +10470,12 @@ Index = (function() { this.lastLongInputs = $$('input', this.lastLongOptions); this.lastLongThresholds = [0, 0]; this.lastLongOptions.hidden = this.selectSort.value !== 'lastlong'; - ref7 = this.lastLongInputs; - for (i = l = 0, len2 = ref7.length; l < len2; i = ++l) { - input = ref7[i]; + ref6 = this.lastLongInputs; + for (i = l = 0, len2 = ref6.length; l < len2; i = ++l) { + input = ref6[i]; $.on(input, 'change', this.cb.lastLongThresholds); tRaw = Conf["Last Long Reply Thresholds " + i]; - input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref8 = tRaw[g.BOARD.ID]) != null ? ref8 : 100 : tRaw; + input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref7 = tRaw[g.BOARD.ID]) != null ? ref7 : 100 : tRaw; } this.root = $.el('div', { className: 'board json-index' @@ -10376,7 +10496,7 @@ Index = (function() { return d.title = d.title.replace(/\ -\ Page\ \d+/, ''); }); $.onExists(doc, '.board > .thread > .postContainer, .board + *', function() { - var board, el, len3, m, ref9, timeEl, topNavPos; + var board, el, len3, m, ref8, timeEl, topNavPos; Build.hat = $('.board > .thread > img:first-child'); if (Build.hat) { g.BOARD.threads.forEach(function(thread) { @@ -10395,9 +10515,9 @@ Index = (function() { try { d.implementation.createDocument(null, null, null).appendChild(board); } catch (_error) {} - ref9 = $$('.navLinks'); - for (m = 0, len3 = ref9.length; m < len3; m++) { - el = ref9[m]; + ref8 = $$('.navLinks'); + for (m = 0, len3 = ref8.length; m < len3; m++) { + el = ref8[m]; $.rm(el); } $.rm($.id('ctrl-top')); @@ -10969,57 +11089,42 @@ Index = (function() { return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? '1 hidden thread' : hiddenCount + " hidden threads"; }, update: function(firstTime) { - var now, ref, ref1; - if ((ref = Index.req) != null) { - ref.abort(); + var oldReq; + if ((oldReq = Index.req)) { + delete Index.req; + oldReq.abort(); } - if ((ref1 = Index.notice) != null) { - ref1.close(); - } - if (Conf['Index Refresh Notifications'] && d.readyState !== 'loading') { - Index.notice = new Notice('info', 'Refreshing index...'); + if (Conf['Index Refresh Notifications']) { + Index.notice || (Index.notice = new Notice('info', 'Refreshing index...')); } else { - now = Date.now(); - $.ready(function() { - return Index.nTimeout = setTimeout((function() { - if (Index.req && !Index.notice) { - return Index.notice = new Notice('info', 'Refreshing index...'); - } - }), 3 * $.SECOND - (Date.now() - now)); - }); + Index.nTimeout || (Index.nTimeout = setTimeout(function() { + return Index.notice || (Index.notice = new Notice('info', 'Refreshing index...')); + }, 3 * $.SECOND)); } if (!firstTime && d.readyState !== 'loading' && !$('.board + *')) { location.reload(); return; } - Index.req = $.ajax(location.protocol + "//a.4cdn.org/" + g.BOARD + "/catalog.json", { - onabort: Index.load, - onloadend: Index.load - }, { - whenModified: 'Index', - bypassCache: true - }); + Index.req = $.whenModified(Site.urls.catalogJSON({ + boardID: g.BOARD.ID + }), 'Index', Index.load); return $.addClass(Index.button, 'fa-spin'); }, - load: function(e) { - var err, nTimeout, notice, ref, req, timeEl; + load: function() { + var err, nTimeout, notice, ref, timeEl; + if (this !== Index.req) { + return; + } $.rmClass(Index.button, 'fa-spin'); - req = Index.req, notice = Index.notice, nTimeout = Index.nTimeout; + notice = Index.notice, nTimeout = Index.nTimeout; if (nTimeout) { clearTimeout(nTimeout); } delete Index.nTimeout; delete Index.req; delete Index.notice; - if (e.type === 'abort') { - req.onloadend = null; - if (notice != null) { - notice.close(); - } - return; - } - if ((ref = req.status) !== 200 && ref !== 304) { - err = "Index refresh failed. " + (req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error'); + if ((ref = this.status) !== 200 && ref !== 304) { + err = "Index refresh failed. " + (this.status ? "Error " + this.statusText + " (" + this.status + ")" : 'Connection Error'); if (notice) { notice.setType('warning'); notice.el.lastElementChild.textContent = err; @@ -11030,9 +11135,9 @@ Index = (function() { return; } try { - if (req.status === 200) { - Index.parse(req.response); - } else if (req.status === 304) { + if (this.status === 200) { + Index.parse(this.response); + } else if (this.status === 304) { Index.pageLoad(); } } catch (_error) { @@ -11057,7 +11162,7 @@ Index = (function() { } } timeEl = $('#index-last-refresh time', Index.navLinks); - timeEl.dataset.utc = Date.parse(req.getResponseHeader('Last-Modified')); + timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified')); return RelativeDates.update(timeEl); }, parse: function(pages) { @@ -11910,20 +12015,27 @@ Settings = (function() { return $.after($('input[name="Stubs"]', section).parentNode.parentNode, div); }, "export": function() { - return $.get(Conf, function(Conf) { - delete Conf['boardConfig']; + var Conf2; + Conf2 = {}; + $.extend(Conf2, Conf); + return $.get(Conf2, function(Conf2) { + delete Conf2['boardConfig']; return Settings.downloadExport({ version: g.VERSION, date: Date.now(), - Conf: Conf + Conf: Conf2 }); }); }, downloadExport: function(data) { - var a, p; + var a, blob, p, url; + blob = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json' + }); + url = URL.createObjectURL(blob); a = $.el('a', { download: "4chan X v" + g.VERSION + "-" + data.date + ".json", - href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))))) + href: url }); p = $('.imp-exp-result', Settings.dialog); $.rmAll(p); @@ -12413,7 +12525,7 @@ Settings = (function() { }; }); $.extend(div, { - innerHTML: "
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.

Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.

" + innerHTML: "
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 and Unique ID filtering use exact string matching, not regular expressions.

Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.

" }); return $('.warning', div).hidden = Conf['Filter']; }, @@ -13120,8 +13232,9 @@ UI = (function() { }; hoverstart = function(arg) { - var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root; - root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove; + var cb, el, endEvents, height, latestEvent, noRemove, o, rect, ref, root, width; + root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, width = arg.width, cb = arg.cb, noRemove = arg.noRemove; + rect = root.getBoundingClientRect(); o = { root: root, el: el, @@ -13133,7 +13246,10 @@ UI = (function() { clientHeight: doc.clientHeight, clientWidth: doc.clientWidth, height: height, - noRemove: noRemove + width: width, + noRemove: noRemove, + clientX: (rect.left + rect.right) / 2, + clientY: (rect.top + rect.bottom) / 2 }; o.hover = hover.bind(o); o.hoverend = hoverend.bind(o); @@ -13161,16 +13277,22 @@ UI = (function() { hoverstart.padding = 25; hover = function(e) { - var clientX, clientY, height, left, ref, right, style, threshold, top; + var clientX, clientY, height, left, marginX, ref, ref1, right, style, threshold, top, width; this.latestEvent = e; height = (this.height || this.el.offsetHeight) + hoverstart.padding; - clientX = e.clientX, clientY = e.clientY; + width = this.width || this.el.offsetWidth; + ref = Conf['Follow Cursor'] ? e : this, clientX = ref.clientX, clientY = ref.clientY; top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120)); threshold = this.clientWidth / 2; if (!this.isImage) { threshold = Math.max(threshold, this.clientWidth - 400); } - ref = clientX <= threshold ? [clientX + 45 + 'px', ''] : ['', this.clientWidth - clientX + 45 + 'px'], left = ref[0], right = ref[1]; + marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45; + if (this.isImage) { + marginX = Math.min(marginX, this.clientWidth - width); + } + marginX += 'px'; + ref1 = clientX <= threshold ? [marginX, ''] : ['', marginX], left = ref1[0], right = ref1[1]; style = this.style; style.top = top + 'px'; style.left = left; @@ -13879,8 +14001,11 @@ ImageCommon = (function() { return cb(URL); } }; - return $.ajax(location.protocol + "//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", { - onload: function() { + return $.ajax(Site.urls.threadJSON({ + boardID: post.boardID, + threadID: post.threadID + }), { + onloadend: function() { var i, len, postObj, ref; if (this.status === 404) { post.kill(!post.isClone); @@ -14454,7 +14579,7 @@ ImageHover = (function() { }, mouseover: function(post) { return function(e) { - var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x; + var el, error, file, height, isVideo, maxHeight, maxWidth, ref, ref1, scale, width, x; if (!doc.contains(this)) { return; } @@ -14490,28 +14615,32 @@ ImageHover = (function() { } } } - ref1 = (function() { - var i, len, ref1, results; - ref1 = file.dimensions.split('x'); - results = []; - for (i = 0, len = ref1.length; i < len; i++) { - x = ref1[i]; - results.push(+x); - } - return results; - })(), width = ref1[0], height = ref1[1]; - ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right; - maxWidth = Math.max(left, doc.clientWidth - right); - maxHeight = doc.clientHeight - UI.hover.padding; - scale = Math.min(1, maxWidth / width, maxHeight / height); - el.style.maxWidth = (scale * width) + "px"; - el.style.maxHeight = (scale * height) + "px"; + if (file.dimensions) { + ref1 = (function() { + var i, len, ref1, results; + ref1 = file.dimensions.split('x'); + results = []; + for (i = 0, len = ref1.length; i < len; i++) { + x = ref1[i]; + results.push(+x); + } + return results; + })(), width = ref1[0], height = ref1[1]; + maxWidth = doc.clientWidth; + maxHeight = doc.clientHeight - UI.hover.padding; + scale = Math.min(1, maxWidth / width, maxHeight / height); + width *= scale; + height *= scale; + el.style.maxWidth = width + "px"; + el.style.maxHeight = height + "px"; + } return UI.hover({ root: this, el: el, latestEvent: e, endEvents: 'mouseout click', - height: scale * height, + height: height, + width: width, noRemove: true, cb: function() { $.off(el, 'error', error); @@ -15369,7 +15498,7 @@ Embedding = (function() { return Embedding.flushTitles(service); } } else { - return CrossOrigin.json(service.api(uid), (function() { + return CrossOrigin.cache(service.api(uid), (function() { return Embedding.cb.title(this, data); })); } @@ -15388,7 +15517,7 @@ Embedding = (function() { Embedding.cb.title(this, data); } }; - return CrossOrigin.json(service.api((function() { + return CrossOrigin.cache(service.api((function() { var j, len, results; results = []; for (j = 0, len = queue.length; j < len; j++) { @@ -15610,7 +15739,7 @@ Embedding = (function() { hidden: true, id: "gist-embed-" + (counter++) }); - CrossOrigin.json("https://api.github.com/gists/" + a.dataset.uid, function() { + CrossOrigin.cache("https://api.github.com/gists/" + a.dataset.uid, function() { el.textContent = Object.values(this.response.files)[0].content; el.className = 'prettyprint'; $.global(function() { @@ -16341,18 +16470,21 @@ DeleteLink = (function() { return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), { responseType: 'document', withCredentials: true, - onload: function() { + onloadend: function() { return DeleteLink.load(link, post, fileOnly, this.response); }, - onerror: function() { - return DeleteLink.error(link, post); - } - }, { form: $.formData(form) }); }, load: function(link, post, fileOnly, resDoc) { var el, msg; + if (!resDoc) { + new Notice('warning', 'Connection error, please retry.', 20); + if (post.fullID === DeleteLink.post.fullID) { + $.on(link, 'click', DeleteLink.toggle); + } + return; + } link.textContent = DeleteLink.linkText(fileOnly); if (resDoc.title === '4chan - Banned') { el = $.el('span', { @@ -16381,12 +16513,6 @@ DeleteLink = (function() { } } }, - error: function(link, post) { - new Notice('warning', 'Connection error, please retry.', 20); - if (post.fullID === DeleteLink.post.fullID) { - return $.on(link, 'click', DeleteLink.toggle); - } - }, cooldown: { seconds: {}, start: function(post, seconds) { @@ -16986,7 +17112,10 @@ ExpandComment = (function() { return; } a.textContent = "Post No." + post + " Loading..."; - return $.cache(location.protocol + "//a.4cdn.org" + (a.pathname.split(/\/+/).splice(0, 4).join('/')) + ".json", function() { + return $.cache(Site.urls.threadJSON({ + boardID: post.boardID, + threadID: post.threadID + }), function() { return ExpandComment.parse(this, a, post); }); }, @@ -17004,7 +17133,7 @@ ExpandComment = (function() { var callback, clone, comment, href, i, j, k, len, len1, len2, postObj, posts, quote, ref, ref1, spoilerRange, status; status = req.status; if (status !== 200 && status !== 304) { - a.textContent = "Error " + req.statusText + " (" + status + ")"; + a.textContent = status ? "Error " + req.statusText + " (" + status + ")" : 'Connection Error'; return; } posts = req.response.posts; @@ -17085,15 +17214,16 @@ ExpandThread = (function() { return $.on(a, 'click', ExpandThread.cbToggle); }, disconnect: function(refresh) { - var ref, ref1, status, threadID; + var oldReq, ref, status, threadID; if (g.VIEW === 'thread' || !Conf['Thread Expansion']) { return; } ref = ExpandThread.statuses; for (threadID in ref) { status = ref[threadID]; - if ((ref1 = status.req) != null) { - ref1.abort(); + if ((oldReq = status.req)) { + delete status.req; + oldReq.abort(); } delete ExpandThread.statuses[threadID]; } @@ -17141,17 +17271,24 @@ ExpandThread = (function() { var status; ExpandThread.statuses[thread] = status = {}; a.textContent = Build.summaryText.apply(Build, ['...'].concat(slice.call(a.textContent.match(/\d+/g)))); - return status.req = $.cache(location.protocol + "//a.4cdn.org/" + thread.board + "/thread/" + thread + ".json", function() { + return status.req = $.cache(Site.urls.threadJSON({ + boardID: thread.board.ID, + threadID: thread.ID + }), function() { + if (this !== status.req) { + return; + } delete status.req; return ExpandThread.parse(this, thread, a); }); }, contract: function(thread, a, threadRoot) { - var filesCount, i, inlined, len, num, postsCount, replies, reply, status; + var filesCount, i, inlined, len, num, oldReq, postsCount, replies, reply, status; status = ExpandThread.statuses[thread]; delete ExpandThread.statuses[thread]; - if (status.req) { - status.req.abort(); + if ((oldReq = status.req)) { + delete status.req; + oldReq.abort(); if (a) { a.textContent = Build.summaryText.apply(Build, ['+'].concat(slice.call(a.textContent.match(/\d+/g)))); } @@ -17201,7 +17338,7 @@ ExpandThread = (function() { parse: function(req, thread, a) { var a2, filesCount, i, len, post, postData, posts, postsCount, postsRoot, ref, ref1, root; if ((ref = req.status) !== 200 && ref !== 304) { - a.textContent = "Error " + req.statusText + " (" + req.status + ")"; + a.textContent = req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error'; return; } Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler; @@ -18736,7 +18873,6 @@ Report = (function() { results = []; fn = function(name, url) { return $.ajax(url, { - responseType: 'json', onloadend: function() { results.push([ name, this.response || { @@ -18746,8 +18882,7 @@ Report = (function() { if (results.length === urls.length) { return cb(results); } - } - }, { + }, form: form }); }; @@ -19379,12 +19514,9 @@ ThreadStats = (function() { return; } ThreadStats.timeout = setTimeout(ThreadStats.fetchPage, 2 * $.MINUTE); - return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadStats.thread.board + "/threads.json", { - onload: ThreadStats.onThreadsLoad - }, { - whenModified: 'ThreadStats', - bypassCache: true - }); + return $.whenModified(Site.urls.threadsListJSON({ + boardID: ThreadStats.thread.board + }), 'ThreadStats', ThreadStats.onThreadsLoad); }, onThreadsLoad: function() { var i, j, k, len, len1, len2, page, purgePos, ref, ref1, ref2, thread; @@ -19593,11 +19725,12 @@ ThreadUpdater = (function() { } }, load: function() { - var req; - req = ThreadUpdater.req; - switch (req.status) { + if (this !== ThreadUpdater.req) { + return; + } + switch (this.status) { case 200: - ThreadUpdater.parse(req); + ThreadUpdater.parse(this); if (ThreadUpdater.thread.isArchived) { return ThreadUpdater.kill(); } else { @@ -19605,7 +19738,9 @@ ThreadUpdater = (function() { } break; case 404: - return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", { + return $.ajax(Site.urls.catalogJSON({ + boardID: ThreadUpdater.thread.board.ID + }), { onloadend: function() { var confirmed, i, k, len, len1, page, ref, ref1, thread; if (this.status === 200) { @@ -19628,12 +19763,12 @@ ThreadUpdater = (function() { if (confirmed) { return ThreadUpdater.kill(); } else { - return ThreadUpdater.error(req); + return ThreadUpdater.error(this); } } }); default: - return ThreadUpdater.error(req); + return ThreadUpdater.error(this); } } }, @@ -19711,18 +19846,18 @@ ThreadUpdater = (function() { return ThreadUpdater.seconds--; }, update: function() { - var ref; + var oldReq; clearTimeout(ThreadUpdater.timeoutID); ThreadUpdater.set('timer', '...', 'loading'); - if ((ref = ThreadUpdater.req) != null) { - ref.abort(); + if ((oldReq = ThreadUpdater.req)) { + delete ThreadUpdater.req; + oldReq.abort(); } - return ThreadUpdater.req = $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/thread/" + ThreadUpdater.thread + ".json", { - onloadend: ThreadUpdater.cb.load, + return ThreadUpdater.req = $.whenModified(Site.urls.threadJSON({ + boardID: ThreadUpdater.thread.board.ID, + threadID: ThreadUpdater.thread.ID + }), 'ThreadUpdater', ThreadUpdater.cb.load, { timeout: $.MINUTE - }, { - whenModified: 'ThreadUpdater', - bypassCache: true }); }, updateThreadStatus: function(type, status) { @@ -19882,6 +20017,7 @@ ThreadWatcher = (function() { className: 'fa fa-eye' }); this.db = new DataBoard('watchedThreads', this.refresh, true); + this.dbLM = new DataBoard('watcherLastModified', null, true); this.dialog = UI.dialog('thread-watcher', { innerHTML: "
Thread Watcher ×
" }); @@ -19912,6 +20048,7 @@ ThreadWatcher = (function() { this.dialog.hidden = true; } Header.addShortcut('watcher', sc, 510); + ThreadWatcher.initLastModified(); ThreadWatcher.fetchAuto(); $.on(window, 'visibilitychange focus', function() { return $.queueTask(ThreadWatcher.fetchAuto); @@ -20148,6 +20285,38 @@ ThreadWatcher = (function() { }, requests: [], fetched: 0, + fetch: function(url, arg, args, cb) { + var ajax, force, onloadend, ref, req, siteID; + siteID = arg.siteID, force = arg.force; + if (ThreadWatcher.requests.length === 0) { + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + } + onloadend = function() { + if (this.finished) { + return; + } + this.finished = true; + ThreadWatcher.fetched++; + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + ThreadWatcher.clearRequests(); + } else { + ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%"; + } + return cb.apply(this, args); + }; + ajax = siteID === Site.hostname ? $.ajax : CrossOrigin.ajax; + if (force) { + if ((ref = $.lastModified.ThreadWatcher) != null) { + delete ref[url]; + } + } + req = $.whenModified(url, 'ThreadWatcher', onloadend, { + timeout: $.MINUTE, + ajax: ajax + }); + return ThreadWatcher.requests.push(req); + }, clearRequests: function() { ThreadWatcher.requests = []; ThreadWatcher.fetched = 0; @@ -20156,15 +20325,44 @@ ThreadWatcher = (function() { }, abort: function() { var i, len1, ref, req; + delete ThreadWatcher.syncing; ref = ThreadWatcher.requests; for (i = 0, len1 = ref.length; i < len1; i++) { req = ref[i]; - if (req.readyState !== 4) { - req.abort(); + if (!(!req.finished)) { + continue; } + req.finished = true; + req.abort(); } return ThreadWatcher.clearRequests(); }, + initLastModified: function() { + var base, boardID, boards, data, date, lm, ref, ref1, siteID, url; + lm = ((base = $.lastModified)['ThreadWatcher'] || (base['ThreadWatcher'] = {})); + ref = ThreadWatcher.dbLM.data; + for (siteID in ref) { + boards = ref[siteID]; + ref1 = boards.boards; + for (boardID in ref1) { + data = ref1[boardID]; + if (ThreadWatcher.db.get({ + siteID: siteID, + boardID: boardID + })) { + for (url in data) { + date = data[url]; + lm[url] = date; + } + } else { + ThreadWatcher.dbLM["delete"]({ + siteID: siteID, + boardID: boardID + }); + } + } + } + }, fetchAuto: function() { var db, interval, now, ref; clearTimeout(ThreadWatcher.timeout); @@ -20176,41 +20374,123 @@ ThreadWatcher = (function() { now = Date.now(); if (!((now - interval < (ref = db.data.lastChecked || 0) && ref <= now) || d.hidden || !d.hasFocus())) { ThreadWatcher.fetchAllStatus(); - db.setLastChecked(); } return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval); }, buttonFetchAll: function() { - if (ThreadWatcher.requests.length) { + if (ThreadWatcher.syncing || ThreadWatcher.requests.length) { return ThreadWatcher.abort(); } else { return ThreadWatcher.fetchAllStatus(); } }, fetchAllStatus: function() { - var db, dbs, i, len1, n, results; + var dbi, dbs, i, len1, n, results; + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + ThreadWatcher.syncing = true; dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(function(x) { return x; }); n = 0; results = []; for (i = 0, len1 = dbs.length; i < len1; i++) { - db = dbs[i]; - results.push(db.forceSync(function() { - var j, len2, thread, threads; + dbi = dbs[i]; + results.push(dbi.forceSync(function() { + var board, boards, db, deep, j, len2, now, ref; if ((++n) === dbs.length) { - threads = ThreadWatcher.getAll(); - for (j = 0, len2 = threads.length; j < len2; j++) { - thread = threads[j]; - ThreadWatcher.fetchStatus(thread); + if (!ThreadWatcher.syncing) { + return; + } + delete ThreadWatcher.syncing; + db = ThreadWatcher.db; + now = Date.now(); + deep = !((now - 2 * $.HOUR < (ref = db.data.lastChecked2 || 0) && ref <= now)); + boards = ThreadWatcher.getAll(true); + for (j = 0, len2 = boards.length; j < len2; j++) { + board = boards[j]; + ThreadWatcher.fetchBoard(board, deep); + } + db.setLastChecked(); + if (deep) { + db.setLastChecked('lastChecked2'); + } + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + return ThreadWatcher.clearRequests(); } } })); } return results; }, + fetchBoard: function(board, deep) { + var base, boardID, ref, ref1, ref2, siteID, software, url, urlF; + if (!board.some(function(thread) { + return !thread.data.isDead; + })) { + return; + } + ref = board[0], siteID = ref.siteID, boardID = ref.boardID; + software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0; + urlF = deep && software === 'tinyboard' ? 'catalogJSON' : 'threadsListJSON'; + url = (ref2 = SW[software]) != null ? typeof (base = ref2.urls)[urlF] === "function" ? base[urlF]({ + siteID: siteID, + boardID: boardID + }) : void 0 : void 0; + if (!url) { + return; + } + return ThreadWatcher.fetch(url, { + siteID: siteID + }, [board, url], ThreadWatcher.parseBoard); + }, + parseBoard: function(board, url) { + var boardID, data, i, item, j, k, len1, len2, len3, lmDate, modified, page, ref, ref1, ref2, replies, siteID, thread, threadID; + if (this.status !== 200) { + return; + } + ref = board[0], siteID = ref.siteID, boardID = ref.boardID; + lmDate = this.getResponseHeader('Last-Modified'); + ThreadWatcher.dbLM.extend({ + siteID: siteID, + boardID: boardID, + val: $.item(url, lmDate) + }); + modified = {}; + replies = {}; + try { + ref1 = this.response; + for (i = 0, len1 = ref1.length; i < len1; i++) { + page = ref1[i]; + ref2 = page.threads; + for (j = 0, len2 = ref2.length; j < len2; j++) { + item = ref2[j]; + modified[item.no] = item.last_modified; + replies[item.no] = item.replies; + } + } + } catch (_error) {} + for (k = 0, len3 = board.length; k < len3; k++) { + thread = board[k]; + threadID = thread.threadID, data = thread.data; + if (modified[threadID]) { + if (modified[threadID] === data.modified && ((replies[threadID] == null) || replies[threadID] === data.replies)) { + continue; + } + ThreadWatcher.db.extend({ + siteID: siteID, + boardID: boardID, + threadID: threadID, + val: { + modified: modified[threadID] + } + }); + } + ThreadWatcher.fetchStatus(thread); + } + }, fetchStatus: function(thread, force) { - var base, boardID, data, ref, ref1, req, siteID, software, threadID, url; + var base, boardID, data, ref, ref1, siteID, software, threadID, url; siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data; software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0; url = (ref1 = SW[software]) != null ? typeof (base = ref1.urls).threadJSON === "function" ? base.threadJSON({ @@ -20227,46 +20507,18 @@ ThreadWatcher = (function() { if (data.last === -1) { return; } - if (ThreadWatcher.requests.length === 0) { - ThreadWatcher.status.textContent = '...'; - $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); - } - if ((typeof Site.hasCORS === "function" ? Site.hasCORS(url) : void 0) || url.split('/').slice(0, 3).join('/') === location.origin) { - req = $.ajax(url, { - onloadend: function() { - return ThreadWatcher.parseStatus.call(this, thread); - }, - timeout: $.MINUTE - }, { - whenModified: force ? false : 'ThreadWatcher' - }); - } else { - req = { - abort: function() { - return req.aborted = true; - } - }; - CrossOrigin.json(url, function() { - if (req.aborted) { - return; - } - return ThreadWatcher.parseStatus.call(this, thread); - }, true, $.MINUTE); - } - return ThreadWatcher.requests.push(req); + return ThreadWatcher.fetch(url, { + siteID: siteID, + force: force + }, [thread], ThreadWatcher.parseStatus); }, parseStatus: function(arg) { - var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, siteID, software, threadID, unread, updated, youOP; + var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, replies, siteID, software, threadID, unread, updated, youOP; siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, data = arg.data; - ThreadWatcher.fetched++; - if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { - ThreadWatcher.clearRequests(); - } else { - ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%"; - } software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0; if (this.status === 200 && this.response) { last = this.response.posts[this.response.posts.length - 1].no; + replies = this.response.posts.length - 1; isDead = !!this.response.posts[0].archived; if (isDead && Conf['Auto Prune']) { ThreadWatcher.db["delete"]({ @@ -20341,6 +20593,7 @@ ThreadWatcher = (function() { threadID: threadID, val: { last: last, + replies: replies, isDead: isDead, unread: unread, quotingYou: quotingYou @@ -20380,8 +20633,8 @@ ThreadWatcher = (function() { return ThreadWatcher.refresh(); } }, - getAll: function() { - var all, boardID, boards, data, ref, ref1, siteID, threadID, threads; + getAll: function(groupByBoard) { + var all, boardID, boards, cont, data, ref, ref1, siteID, threadID, threads; all = []; ref = ThreadWatcher.db.data; for (siteID in ref) { @@ -20392,10 +20645,13 @@ ThreadWatcher = (function() { if (Conf['Current Board'] && (siteID !== Site.hostname || boardID !== g.BOARD.ID)) { continue; } + if (groupByBoard) { + all.push((cont = [])); + } for (threadID in threads) { data = threads[threadID]; if (data && typeof data === 'object') { - all.push({ + (groupByBoard ? cont : all).push({ siteID: siteID, boardID: boardID, threadID: threadID, @@ -20949,12 +21205,15 @@ Unread = (function() { return; } }, - openNotification: function(post) { + openNotification: function(post, predicate) { var notif; + if (predicate == null) { + predicate = ' replied to you'; + } if (!Header.areNotificationsEnabled) { return; } - notif = new Notification(post.info.nameBlock + " replied to you", { + notif = new Notification("" + post.info.nameBlock + predicate, { body: post.commentDisplay(), icon: Favicon.logo }); @@ -22730,7 +22989,7 @@ QR = (function() { } }, submit: function(e) { - var captcha, cb, err, extra, filetag, formData, options, post, ref, thread, threadID; + var captcha, cb, err, filetag, formData, options, post, ref, thread, threadID; if (e != null) { e.preventDefault(); } @@ -22800,47 +23059,35 @@ QR = (function() { options = { responseType: 'document', withCredentials: true, - onload: QR.response, - onerror: function() { - delete QR.req; - if (QR.currentCaptcha) { - Captcha.cache.save(QR.currentCaptcha); - } - delete QR.currentCaptcha; - post.unlock(); - QR.cooldown.auto = true; - QR.cooldown.addDelay(post, 2); - QR.status(); - return QR.error(QR.connectionError()); - } - }; - extra = { + onloadend: QR.response, form: $.formData(formData) }; if (Conf['Show Upload Progress']) { - extra.upCallbacks = { - onload: function() { + options.onprogress = function(e) { + var ref1; + if (this !== ((ref1 = QR.req) != null ? ref1.upload : void 0)) { + return; + } + if (e.loaded < e.total) { + QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%"; + } else { QR.req.isUploadFinished = true; QR.req.progress = '...'; - return QR.status(); - }, - onprogress: function(e) { - QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%"; - return QR.status(); } + return QR.status(); }; } cb = function(response) { if (response != null) { QR.currentCaptcha = response; if (response.challenge != null) { - extra.form.append('recaptcha_challenge_field', response.challenge); - extra.form.append('recaptcha_response_field', response.response); + options.form.append('recaptcha_challenge_field', response.challenge); + options.form.append('recaptcha_response_field', response.response); } else { - extra.form.append('g-recaptcha-response', response.response); + options.form.append('g-recaptcha-response', response.response); } } - QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options, extra); + QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options); return QR.req.progress = '...'; }; if (typeof captcha === 'function') { @@ -22866,23 +23113,24 @@ QR = (function() { return QR.status(); }, response: function() { - var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, req, resDoc, seconds, threadID; - req = QR.req; + var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, ref3, seconds, threadID; + if (this !== QR.req) { + return; + } delete QR.req; post = QR.posts[0]; post.unlock(); - resDoc = req.response; - if ((err = resDoc.getElementById('errmsg'))) { - if ((ref = $('a', err)) != null) { - ref.target = '_blank'; + if ((err = (ref = this.response) != null ? ref.getElementById('errmsg') : void 0)) { + if ((ref1 = $('a', err)) != null) { + ref1.target = '_blank'; } - } else if ((connErr = resDoc.title !== 'Post successful!')) { + } else if ((connErr = !this.response || this.response.title !== 'Post successful!')) { err = QR.connectionError(); if (QR.currentCaptcha) { Captcha.cache.save(QR.currentCaptcha); } - } else if (req.status !== 200) { - err = "Error " + req.statusText + " (" + req.status + ")"; + } else if (this.status !== 200) { + err = "Error " + this.statusText + " (" + this.status + ")"; } delete QR.currentCaptcha; if (err) { @@ -22905,13 +23153,13 @@ QR = (function() { } else { QR.cooldown.auto = false; } - QR.captcha.setup(QR.cooldown.auto && ((ref1 = d.activeElement) === QR.nodes.status || ref1 === d.body)); + QR.captcha.setup(QR.cooldown.auto && ((ref2 = d.activeElement) === QR.nodes.status || ref2 === d.body)); QR.status(); QR.error(err); return; } - h1 = $('h1', resDoc); - ref2 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref2[0], threadID = ref2[1], postID = ref2[2]; + h1 = $('h1', this.response); + ref3 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref3[0], threadID = ref3[1], postID = ref3[2]; postID = +postID; threadID = +threadID || postID; isReply = threadID !== postID; @@ -22928,10 +23176,10 @@ QR = (function() { postsCount = QR.posts.length - 1; QR.cooldown.auto = postsCount && isReply; lastPostToThread = !((function() { - var j, len, p, ref3; - ref3 = QR.posts.slice(1); - for (j = 0, len = ref3.length; j < len; j++) { - p = ref3[j]; + var j, len, p, ref4; + ref4 = QR.posts.slice(1); + for (j = 0, len = ref4.length; j < len; j++) { + p = ref4[j]; if (p.thread === post.thread) { return true; } @@ -22982,17 +23230,18 @@ QR = (function() { } else { return setTimeout(check, attempts * $.SECOND); } - } - }, { + }, + responseType: 'text', type: 'HEAD' }); }; return check(); }, abort: function() { - if (QR.req && !QR.req.isUploadFinished) { - QR.req.abort(); + var oldReq; + if ((oldReq = QR.req) && !QR.req.isUploadFinished) { delete QR.req; + oldReq.abort(); if (QR.currentCaptcha) { Captcha.cache.save(QR.currentCaptcha); } diff --git a/builds/4chan-X.crx b/builds/4chan-X.crx index e2f66b84a..f5a9e2625 100644 Binary files a/builds/4chan-X.crx and b/builds/4chan-X.crx differ diff --git a/builds/4chan-X.meta.js b/builds/4chan-X.meta.js index 35ddd95fa..a601da04e 100644 --- a/builds/4chan-X.meta.js +++ b/builds/4chan-X.meta.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X -// @version 1.14.5.14 +// @version 1.14.6.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js index 0c5cae95f..4979a14d6 100644 --- a/builds/4chan-X.user.js +++ b/builds/4chan-X.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan X -// @version 1.14.5.14 +// @version 1.14.6.0 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X @@ -198,7 +198,7 @@ docSet = function() { }; g = { - VERSION: '1.14.5.14', + VERSION: '1.14.6.0', NAMESPACE: '4chan X.', boards: {} }; @@ -241,6 +241,7 @@ Config = (function() { 'JSON Index': [true, 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'], 'Use 4chan X Catalog': [true, 'Link to 4chan X\'s catalog instead of the native 4chan one.', 1], 'Index Refresh Notifications': [false, 'Show a notice at the top of the page when the index is refreshed.', 1], + 'Follow Cursor': [true, 'Image Hover and Quote Preview move with the mouse cursor.'], 'Open Threads in New Tab': [false, 'Make links to threads in the index / 4chan X catalog open in a new tab.'], 'External Catalog': [false, 'Link to external catalog instead of the internal one.'], 'Catalog Links': [false, 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'], @@ -1390,6 +1391,10 @@ body.is_catalog .thread > a > img {\n\ .nwsb {\n\ display: inline;\n\ }\n\ +.fileText {\n\ + max-width: auto;\n\ + white-space: normal;\n\ +}\n\ /* Ads */\n\ .ad-cnt > *, .adg-rects > *, .bsa-cnt {\n\ height: auto !important;\n\ @@ -2587,6 +2592,13 @@ span.hide-announcement {\n\ .fileThumb > .warning {\n\ clear: both;\n\ }\n\ +#ihover {\n\ + pointer-events: none;\n\ + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */\n\ + max-height: 95vh;\n\ + max-height: calc(100vh - 25px);\n\ + max-width: 100vw;\n\ +}\n\ /* WEBM Metadata */\n\ .webm-title > a::before {\n\ content: \"title\";\n\ @@ -4595,58 +4607,43 @@ $ = (function() { }; $.ajax = (function() { - var lastModified, pageXHR; - lastModified = {}; + var pageXHR; if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) { pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest); } else { pageXHR = XMLHttpRequest; } - return function(url, options, extra) { - var bypassCache, err, event, form, j, len, params, r, ref, ref1, type, upCallbacks, url0, whenModified; + return function(url, options) { + var err, form, headers, key, onloadend, onprogress, r, ref, responseType, timeout, type, value, withCredentials; if (options == null) { options = {}; } - if (extra == null) { - extra = {}; - } - type = extra.type, whenModified = extra.whenModified, bypassCache = extra.bypassCache, upCallbacks = extra.upCallbacks, form = extra.form; - if (/\.json$/.test(url)) { - if (options.responseType == null) { - options.responseType = 'json'; - } + onloadend = options.onloadend, timeout = options.timeout, responseType = options.responseType, withCredentials = options.withCredentials, type = options.type, onprogress = options.onprogress, form = options.form, headers = options.headers; + if (responseType == null) { + responseType = 'json'; } url = url.replace(/^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'); - if (whenModified) { - params = []; - if ($.engine === 'blink') { - params.push("s=" + whenModified); - } - if (Site.software === 'yotsuba' && bypassCache) { - params.push("t=" + (Date.now())); - } - url0 = url; - if (params.length) { - url += '?' + params.join('&'); - } - } r = new pageXHR(); type || (type = form && 'post' || 'get'); try { r.open(type, url, true); - if (whenModified) { - if (((ref = lastModified[whenModified]) != null ? ref[url0] : void 0) != null) { - r.setRequestHeader('If-Modified-Since', lastModified[whenModified][url0]); - } - $.on(r, 'load', function() { - return (lastModified[whenModified] || (lastModified[whenModified] = {}))[url0] = r.getResponseHeader('Last-Modified'); - }); + ref = headers || {}; + for (key in ref) { + value = ref[key]; + r.setRequestHeader(key, value); } - $.extend(r, options); - $.extend(r.upload, upCallbacks); + $.extend(r, { + onloadend: onloadend, + timeout: timeout, + responseType: responseType, + withCredentials: withCredentials + }); + $.extend(r.upload, { + onprogress: onprogress + }); $.on(r, 'error', function() { if (!r.status) { - return c.error("4chan X failed to load: " + url); + return c.warn("4chan X failed to load: " + url); } }); r.send(form); @@ -4655,51 +4652,82 @@ $ = (function() { if (err.result !== 0x805e0006) { throw err; } - ref1 = ['error', 'loadend']; - for (j = 0, len = ref1.length; j < len; j++) { - event = ref1[j]; - r["on" + event] = options["on" + event]; - $.queueTask($.event, event, null, r); - } + r.onloadend = onloadend; + $.queueTask($.event, 'error', null, r); + $.queueTask($.event, 'loadend', null, r); } return r; }; })(); + $.lastModified = {}; + + $.whenModified = function(url, bucket, cb, options) { + var ajax, headers, params, r, ref, t, timeout, url0; + if (options == null) { + options = {}; + } + timeout = options.timeout, ajax = options.ajax; + params = []; + if ($.engine === 'blink') { + params.push("s=" + bucket); + } + if (Site.software === 'yotsuba') { + params.push("t=" + (Date.now())); + } + url0 = url; + if (params.length) { + url += '?' + params.join('&'); + } + headers = {}; + if ((t = (ref = $.lastModified[bucket]) != null ? ref[url0] : void 0) != null) { + headers['If-Modified-Since'] = t; + } + r = (ajax || $.ajax)(url, { + onloadend: function() { + var base; + ((base = $.lastModified)[bucket] || (base[bucket] = {}))[url0] = this.getResponseHeader('Last-Modified'); + return cb.call(this); + }, + timeout: timeout, + headers: headers + }); + return r; + }; + (function() { var reqs; reqs = {}; $.cache = function(url, cb, options) { - var err, req, rm; - if (req = reqs[url]) { - if (req.readyState === 4) { - $.queueTask(function() { - return cb.call(req, req.evt, true); - }); - } else { + var ajax, onloadend, req; + if (options == null) { + options = {}; + } + ajax = options.ajax; + if ((req = reqs[url])) { + if (req.callbacks) { req.callbacks.push(cb); + } else { + $.queueTask(function() { + return cb.call(req, { + isCached: true + }); + }); } return req; } - rm = function() { - return delete reqs[url]; - }; - try { - if (!(req = $.ajax(url, options))) { - return; - } - } catch (_error) { - err = _error; - return; - } - $.on(req, 'load', function(e) { + onloadend = function() { var fn1, j, len, ref; - this.evt = e; + if (!this.status) { + delete reqs[url]; + } ref = this.callbacks; fn1 = (function(_this) { return function(cb) { return $.queueTask(function() { - return cb.call(_this, e, false); + return cb.call(_this, { + isCached: false + }); }); }; })(this); @@ -4708,8 +4736,10 @@ $ = (function() { fn1(cb); } return delete this.callbacks; + }; + req = (ajax || $.ajax)(url, { + onloadend: onloadend }); - $.on(req, 'abort error', rm); req.callbacks = [cb]; return reqs[url] = req; }; @@ -5402,7 +5432,7 @@ $$ = (function() { }).call(this); CrossOrigin = (function() { - var CrossOrigin; + var CrossOrigin, Request; CrossOrigin = { binary: function(url, cb, headers) { @@ -5471,95 +5501,94 @@ CrossOrigin = (function() { return cb(blob); }); }, - json: (function() { - var callbacks, failure, results, success; - callbacks = {}; - results = {}; - success = function(url, result) { - var cb, j, len, ref; - ref = callbacks[url]; - for (j = 0, len = ref.length; j < len; j++) { - cb = ref[j]; - $.queueTask(function() { - return cb.call(result); - }); - } - delete callbacks[url]; - return results[url] = result; - }; - failure = function(url) { - var cb, j, len, ref; - ref = callbacks[url]; - for (j = 0, len = ref.length; j < len; j++) { - cb = ref[j]; - $.queueTask(function() { - return cb.call({}); - }); - } - return delete callbacks[url]; - }; - return function(url, cb, bypassCache, timeout) { - var req; - if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) { - if (bypassCache) { - $.cleanCache(function(url2) { - return url2 === url; - }); - } - if ((req = $.cache(url, cb, { - responseType: 'json' - }))) { - $.on(req, 'abort error', function() { - return cb.call({}); - }); - } else { - cb.call({}); - } - return; - } - if (bypassCache) { - delete results[url]; - } else { - if (results[url]) { - cb.call(results[url]); - return; - } - if (callbacks[url]) { - callbacks[url].push(cb); - return; - } - } - callbacks[url] = [cb]; - return ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({ - method: "GET", - url: url + '', - timeout: timeout, - onload: function(xhr) { - var response, status, statusText; - status = xhr.status, statusText = xhr.statusText; - try { - response = JSON.parse(xhr.responseText); - return success(url, { - status: status, - statusText: statusText, - response: response - }); - } catch (_error) { - return failure(url); + Request: Request = (function() { + function Request() {} + + Request.prototype.status = 0; + + Request.prototype.statusText = ''; + + Request.prototype.response = null; + + Request.prototype.responseHeaderString = null; + + Request.prototype.getResponseHeader = function(headerName) { + var header, i, j, key, len, ref, ref1, val; + if ((this.responseHeaders == null) && (this.responseHeaderString != null)) { + this.responseHeaders = {}; + ref = this.responseHeaderString.split('\r\n'); + for (j = 0, len = ref.length; j < len; j++) { + header = ref[j]; + if ((i = header.indexOf(':')) >= 0) { + key = header.slice(0, i).trim().toLowerCase(); + val = header.slice(i + 1).trim(); + this.responseHeaders[key] = val; } - }, - onerror: function() { - return failure(url); - }, - onabort: function() { - return failure(url); - }, - ontimeout: function() { - return failure(url); } - }); + } + return (ref1 = (this.responseHeaders || {})[headerName.toLowerCase()]) != null ? ref1 : null; }; + + Request.prototype.abort = function() {}; + + Request.prototype.onloadend = function() {}; + + return Request; + })(), + ajax: function(url, options) { + var gmReq, headers, onloadend, req, timeout; + if (options == null) { + options = {}; + } + onloadend = options.onloadend, timeout = options.timeout, headers = options.headers; + if (!(((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) != null) || (typeof GM_xmlhttpRequest !== "undefined" && GM_xmlhttpRequest !== null))) { + return $.ajax(url, options); + } + req = new CrossOrigin.Request(); + req.onloadend = onloadend; + gmReq = ((typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : void 0) || GM_xmlhttpRequest)({ + method: 'GET', + url: url, + headers: headers, + timeout: timeout, + onload: function(xhr) { + var response; + try { + response = xhr.responseText ? JSON.parse(xhr.responseText) : null; + $.extend(req, { + response: response, + status: xhr.status, + statusText: xhr.statusText, + responseHeaderString: xhr.responseHeaders + }); + } catch (_error) {} + return req.onloadend(); + }, + onerror: function() { + return req.onloadend(); + }, + onabort: function() { + return req.onloadend(); + }, + ontimeout: function() { + return req.onloadend(); + } + }); + if (typeof gmReq.abort === 'function') { + req.abort = function() { + try { + return gmReq.abort(); + } catch (_error) {} + }; + } + return req; + }, + cache: function(url, cb) { + return $.cache(url, cb, { + ajax: CrossOrigin.ajax + }); + }, permission: function(cb) { return cb(); } @@ -5764,7 +5793,7 @@ DataBoard = (function() { bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; DataBoard = (function() { - DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']; + DataBoard.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']; function DataBoard(key1, sync, dontClean) { var init; @@ -5952,7 +5981,7 @@ DataBoard = (function() { boardID: boardID, threadID: threadID, postID: postID, - val: {} + defaultValue: {} }); ref = rm || []; for (i = 0, len = ref.length; i < len; i++) { @@ -5971,10 +6000,13 @@ DataBoard = (function() { })(this), cb); }; - DataBoard.prototype.setLastChecked = function() { + DataBoard.prototype.setLastChecked = function(key) { + if (key == null) { + key = 'lastChecked'; + } return this.save((function(_this) { return function() { - return _this.data.lastChecked = Date.now(); + return _this.data[key] = Date.now(); }; })(this)); }; @@ -6005,9 +6037,6 @@ DataBoard = (function() { DataBoard.prototype.clean = function() { var boardID, now, ref, ref1, siteID, val; - if (Site.software !== 'yotsuba') { - return; - } siteID = Site.hostname; ref = this.data[siteID].boards; for (boardID in ref) { @@ -6027,21 +6056,36 @@ DataBoard = (function() { }; DataBoard.prototype.ajaxClean = function(boardID) { - return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/threads.json", (function(_this) { - return function(e1) { - var response1; - if (e1.target.status !== 200) { + var base, siteID, that, threadsList; + that = this; + siteID = Site.hostname; + threadsList = typeof (base = Site.urls).threadsListJSON === "function" ? base.threadsListJSON({ + siteID: siteID, + boardID: boardID + }) : void 0; + if (!threadsList) { + return; + } + return $.cache(threadsList, function() { + var archiveList, base1, response1; + if (this.status !== 200) { + return; + } + archiveList = typeof (base1 = Site.urls).archiveListJSON === "function" ? base1.archiveListJSON({ + siteID: siteID, + boardID: boardID + }) : void 0; + if (!archiveList) { + return that.ajaxCleanParse(boardID, this.response); + } + response1 = this.response; + return $.cache(archiveList, function() { + if (this.status !== 200) { return; } - response1 = e1.target.response; - return $.cache(location.protocol + "//a.4cdn.org/" + boardID + "/archive.json", function(e2) { - if (!(e2.target.status === 200 || (boardID === 'b' || boardID === 'f' || boardID === 'trash' || boardID === 'bant'))) { - return; - } - return _this.ajaxCleanParse(boardID, response1, e2.target.response); - }); - }; - })(this)); + return that.ajaxCleanParse(boardID, response1, this.response); + }); + }); }; DataBoard.prototype.ajaxCleanParse = function(boardID, response1, response2) { @@ -6102,7 +6146,7 @@ Fetcher = (function() { Fetcher = (function() { function Fetcher(boardID1, threadID, postID1, root, quoter) { - var board, post, ref, thread; + var board, post, ref, that, thread; this.boardID = boardID1; this.threadID = threadID; this.postID = postID1; @@ -6122,11 +6166,15 @@ Fetcher = (function() { } this.root.textContent = "Loading post No." + this.postID + "..."; if (this.threadID) { - $.cache(location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json", (function(_this) { - return function(e, isCached) { - return _this.fetchedPost(e.target, isCached); - }; - })(this)); + that = this; + $.cache(Site.urls.threadJSON({ + boardID: this.boardID, + threadID: this.threadID + }), function(arg) { + var isCached; + isCached = arg.isCached; + return that.fetchedPost(this, isCached); + }); } else { this.archivedPost(); } @@ -6165,18 +6213,18 @@ Fetcher = (function() { }; Fetcher.prototype.fetchedPost = function(req, isCached) { - var api, board, k, len, post, posts, status, thread; + var api, board, k, len, post, posts, status, that, thread; if (post = g.posts[this.boardID + "." + this.postID]) { this.insert(post); return; } status = req.status; if (status !== 200) { - if (this.archivedPost()) { + if (status && this.archivedPost()) { return; } $.addClass(this.root, 'warning'); - this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : "Error " + req.statusText + " (" + req.status + ")."; + this.root.textContent = status === 404 ? "Thread No." + this.threadID + " 404'd." : !status ? 'Connection Error' : "Error " + req.statusText + " (" + req.status + ")."; return; } posts = req.response.posts; @@ -6189,15 +6237,17 @@ Fetcher = (function() { } if (post.no !== this.postID) { if (isCached) { - api = location.protocol + "//a.4cdn.org/" + this.boardID + "/thread/" + this.threadID + ".json"; + api = Site.urls.threadJSON({ + boardID: this.boardID, + threadID: this.threadID + }); $.cleanCache(function(url) { return url === api; }); - $.cache(api, (function(_this) { - return function(e) { - return _this.fetchedPost(e.target, false); - }; - })(this)); + that = this; + $.cache(api, function() { + return that.fetchedPost(this, false); + }); return; } if (this.archivedPost()) { @@ -6230,7 +6280,7 @@ Fetcher = (function() { encryptionOK = /^https:\/\//.test(url) || location.protocol === 'http:'; if (encryptionOK || Conf['Exempt Archives from Encryption']) { that = this; - CrossOrigin.json(url, function() { + CrossOrigin.cache(url, function() { var key, media, ref, ref1; if (!encryptionOK && ((ref = this.response) != null ? ref.media : void 0)) { media = this.response.media; @@ -7359,6 +7409,26 @@ SW = {}; } else { return ''; } + }, + threadsListJSON: function(arg) { + var boardID, ref, root, siteID; + siteID = arg.siteID, boardID = arg.boardID; + root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0; + if (root) { + return "" + root + boardID + "/threads.json"; + } else { + return ''; + } + }, + catalogJSON: function(arg) { + var boardID, ref, root, siteID; + siteID = arg.siteID, boardID = arg.boardID; + root = (ref = Conf['siteProperties'][siteID]) != null ? ref.root : void 0; + if (root) { + return "" + root + boardID + "/catalog.json"; + } else { + return ''; + } } }, selectors: { @@ -7475,6 +7545,25 @@ SW = {}; var boardID, threadID; boardID = arg.boardID, threadID = arg.threadID; return location.protocol + "//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json"; + }, + threadsListJSON: function(arg) { + var boardID; + boardID = arg.boardID; + return location.protocol + "//a.4cdn.org/" + boardID + "/threads.json"; + }, + archiveListJSON: function(arg) { + var boardID; + boardID = arg.boardID; + if (BoardConfig.isArchived(boardID)) { + return location.protocol + "//a.4cdn.org/" + boardID + "/archive.json"; + } else { + return ''; + } + }, + catalogJSON: function(arg) { + var boardID; + boardID = arg.boardID; + return location.protocol + "//a.4cdn.org/" + boardID + "/catalog.json"; } }, selectors: { @@ -7565,7 +7654,10 @@ SW = {}; thread.ipCount = (m = scriptData.match(/\bunique_ips *= *(\d+)\b/)) ? +m[1] : void 0; if (g.BOARD.ID === 'f' && thread.OP.file) { file = thread.OP.file; - return $.ajax(location.protocol + "//a.4cdn.org/f/thread/" + thread + ".json", { + return $.ajax(Site.urls.threadJSON({ + boardID: 'f', + threadID: thread.ID + }), { timeout: $.MINUTE, onloadend: function() { if (this.response) { @@ -7860,7 +7952,9 @@ Redirect = (function() { response: response }); } else { - CrossOrigin.json(url, load(i), true); + CrossOrigin.ajax(url, { + onloadend: load(i) + }); } } } else { @@ -8025,7 +8119,7 @@ Filter = (function() { filters: {}, results: {}, init: function() { - var base, base1, boards, err, excludes, filter, hl, i, j, key, len, len1, line, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, sfwBoards, stub, top, type, types; + var base, base1, board, boards, boardsRaw, err, excludes, excludesRaw, file, filter, hide, hl, i, isstring, j, k, key, l, len, len1, len2, len3, line, mask, noti, nsfwBoards, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, regexp, sfwBoards, stub, top, type, types; if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) { return; } @@ -8041,16 +8135,33 @@ Filter = (function() { if (line[0] === '#') { continue; } - if (!(regexp = line.match(/\/(.+)\/(\w*)/))) { + if (!(regexp = line.match(/\/(.*)\/(\w*)/))) { continue; } filter = line.replace(regexp[0], ''); - boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global'; - boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards); - boards = boards === 'global' ? null : boards.split(','); - excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase() : void 0) || null; - excludes = excludes === null ? null : excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); - if (key === 'uniqueID' || key === 'MD5') { + boardsRaw = (ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0; + if (boardsRaw) { + boards = {}; + ref3 = boardsRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); + for (j = 0, len1 = ref3.length; j < len1; j++) { + board = ref3[j]; + boards[board] = true; + } + } else { + boards = false; + } + excludesRaw = (ref4 = filter.match(/exclude:([^;]+)/)) != null ? ref4[1].toLowerCase() : void 0; + if (excludesRaw) { + excludes = {}; + ref5 = excludesRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(','); + for (k = 0, len2 = ref5.length; k < len2; k++) { + board = ref5[k]; + excludes[board] = true; + } + } else { + excludes = false; + } + if ((isstring = (key === 'uniqueID' || key === 'MD5'))) { regexp = regexp[1]; } else { try { @@ -8061,10 +8172,19 @@ Filter = (function() { continue; } } - op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes'; + op = ((ref6 = filter.match(/[^t]op:(no|only)/)) != null ? ref6[1] : void 0) || ''; + mask = { + 'no': 1, + 'only': 2 + }[op] || 0; + file = ((ref7 = filter.match(/file:(no|only)/)) != null ? ref7[1] : void 0) || ''; + mask = mask | ({ + 'no': 4, + 'only': 8 + }[file] || 0); stub = (function() { - var ref5; - switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) { + var ref8; + switch ((ref8 = filter.match(/stub:(yes|no)/)) != null ? ref8[1] : void 0) { case 'yes': return true; case 'no': @@ -8073,9 +8193,10 @@ Filter = (function() { return Conf['Stubs']; } })(); - if (hl = /highlight/.test(filter)) { - hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight'; - top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes'; + noti = /notify/.test(filter); + if ((hl = /highlight/.test(filter))) { + hl = ((ref8 = filter.match(/highlight:([\w-]+)/)) != null ? ref8[1] : void 0) || 'filter-highlight'; + top = ((ref9 = filter.match(/top:(yes|no)/)) != null ? ref9[1] : void 0) || 'yes'; top = top === 'yes'; } if (key === 'general') { @@ -8087,10 +8208,22 @@ Filter = (function() { types = ['subject', 'name', 'filename', 'comment']; } } - filter = this.createFilter(regexp, boards, excludes, op, stub, hl, top); + hide = !(hl || noti); + filter = { + isstring: isstring, + regexp: regexp, + boards: boards, + excludes: excludes, + mask: mask, + hide: hide, + stub: stub, + hl: hl, + top: top, + noti: noti + }; if (key === 'general') { - for (j = 0, len1 = types.length; j < len1; j++) { - type = types[j]; + for (l = 0, len3 = types.length; l < len3; l++) { + type = types[l]; ((base = this.filters)[type] || (base[type] = [])).push(filter); } } else { @@ -8106,37 +8239,8 @@ Filter = (function() { cb: this.node }); }, - createFilter: function(regexp, boards, excludes, op, stub, hl, top) { - var settings, test; - test = typeof regexp === 'string' ? function(value) { - return regexp === value; - } : function(value) { - return regexp.test(value); - }; - settings = { - hide: !hl, - stub: stub, - "class": hl, - top: top - }; - return function(value, boardID, isReply) { - if (boards && indexOf.call(boards, boardID) < 0) { - return false; - } - if (excludes && indexOf.call(excludes, boardID) >= 0) { - return false; - } - if (isReply && op === 'only' || !isReply && op === 'no') { - return false; - } - if (!test(value)) { - return false; - } - return settings; - }; - }, test: function(post, hideable) { - var filter, hide, hl, i, key, len, ref, ref1, result, stub, top, value; + var filter, hide, hl, i, key, len, mask, noti, ref, ref1, stub, top, value; if (hideable == null) { hideable = true; } @@ -8147,25 +8251,32 @@ Filter = (function() { stub = true; hl = void 0; top = false; + noti = false; if (QuoteYou.isYou(post)) { hideable = false; } + mask = (post.isReply ? 2 : 1); + mask = mask | (post.file ? 4 : 8); for (key in Filter.filters) { if (((value = Filter[key](post)) != null)) { ref = Filter.filters[key]; for (i = 0, len = ref.length; i < len; i++) { filter = ref[i]; - if ((result = filter(value, post.boardID, post.isReply))) { - if (result.hide) { - if (hideable) { - hide = true; - stub && (stub = result.stub); - } - } else { - if (!(hl && (ref1 = result["class"], indexOf.call(hl, ref1) >= 0))) { - (hl || (hl = [])).push(result["class"]); - } - top || (top = result.top); + if ((filter.boards && !filter.boards[post.boardID]) || (filter.excludes && filter.excludes[post.boardID]) || (filter.mask & mask) || (filter.isstring ? filter.regexp !== value : !filter.regexp.test(value))) { + continue; + } + if (filter.hide) { + if (hideable) { + hide = true; + stub && (stub = filter.stub); + } + } else { + if (!(hl && (ref1 = filter.hl, indexOf.call(hl, ref1) >= 0))) { + (hl || (hl = [])).push(filter.hl); + } + top || (top = filter.top); + if (filter.noti) { + noti = true; } } } @@ -8179,16 +8290,17 @@ Filter = (function() { } else { return { hl: hl, - top: top + top: top, + noti: noti }; } }, node: function() { - var hide, hl, ref, stub, top; + var hide, hl, noti, ref, stub, top; if (this.isClone) { return; } - ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top; + ref = Filter.test(this, !this.isFetchedQuote && (this.isReply || g.VIEW === 'index')), hide = ref.hide, stub = ref.stub, hl = ref.hl, top = ref.top, noti = ref.noti; if (hide) { if (this.isReply) { PostHiding.hide(this, stub); @@ -8201,6 +8313,9 @@ Filter = (function() { $.addClass.apply($, [this.nodes.root].concat(slice.call(hl))); } } + if (noti && Unread.posts && (this.ID > Unread.lastReadPost) && !QuoteYou.isYou(this)) { + return Unread.openNotification(this, ' triggered a notification filter'); + } }, isHidden: function(post) { return !!Filter.test(post).hide; @@ -8212,7 +8327,7 @@ Filter = (function() { return post.info.name; }, uniqueID: function(post) { - return post.info.uniqueID; + return post.info.uniqueID || ''; }, tripcode: function(post) { return post.info.tripcode; @@ -9137,6 +9252,11 @@ BoardConfig = (function() { domain: function(board) { return "boards." + (BoardConfig.isSFW(board) ? '4channel' : '4chan') + ".org"; }, + isArchived: function(board) { + var data; + data = (this.boards || Conf['boardConfig'].boards)[board]; + return !data || data.is_archived; + }, noAudio: function(boardID) { var boards; if (Site.software !== 'yotsuba') { @@ -9904,7 +10024,7 @@ Header = (function() { } } if (/-expired/.test(t)) { - if (boardID !== 'b' && boardID !== 'f' && boardID !== 'trash' && boardID !== 'bant') { + if (BoardConfig.isArchived(boardID)) { a.href = "//" + (BoardConfig.domain(boardID)) + "/" + boardID + "/archive"; } else { return a.firstChild; @@ -10230,7 +10350,7 @@ Index = (function() { showHiddenThreads: false, changed: {}, init: function() { - var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, select, sortEntry, tRaw, watchSettings; + var arr, entries, i, input, inputs, k, l, label, len1, len2, name, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, select, sortEntry, tRaw, watchSettings; if (!(g.VIEW === 'index' && g.BOARD.ID !== 'f')) { return; } @@ -10320,7 +10440,7 @@ Index = (function() { innerHTML: "Index Catalog Archive Bottom ×" }); $('.cataloglink a', this.navLinks).href = CatalogLinks.catalog(); - if ((ref5 = g.BOARD.ID) === 'b' || ref5 === 'trash' || ref5 === 'bant') { + if (!BoardConfig.isArchived(g.BOARD.ID)) { $('.archlistlink', this.navLinks).hidden = true; } $.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront); @@ -10339,9 +10459,9 @@ Index = (function() { $.on(this.selectSort, 'change', this.cb.sort); $.on(this.selectSize, 'change', $.cb.value); $.on(this.selectSize, 'change', this.cb.size); - ref6 = [this.selectMode, this.selectSize]; - for (k = 0, len1 = ref6.length; k < len1; k++) { - select = ref6[k]; + ref5 = [this.selectMode, this.selectSize]; + for (k = 0, len1 = ref5.length; k < len1; k++) { + select = ref5[k]; select.value = Conf[select.name]; } this.selectRev.checked = /-rev$/.test(Index.currentSort); @@ -10350,12 +10470,12 @@ Index = (function() { this.lastLongInputs = $$('input', this.lastLongOptions); this.lastLongThresholds = [0, 0]; this.lastLongOptions.hidden = this.selectSort.value !== 'lastlong'; - ref7 = this.lastLongInputs; - for (i = l = 0, len2 = ref7.length; l < len2; i = ++l) { - input = ref7[i]; + ref6 = this.lastLongInputs; + for (i = l = 0, len2 = ref6.length; l < len2; i = ++l) { + input = ref6[i]; $.on(input, 'change', this.cb.lastLongThresholds); tRaw = Conf["Last Long Reply Thresholds " + i]; - input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref8 = tRaw[g.BOARD.ID]) != null ? ref8 : 100 : tRaw; + input.value = this.lastLongThresholds[i] = typeof tRaw === 'object' ? (ref7 = tRaw[g.BOARD.ID]) != null ? ref7 : 100 : tRaw; } this.root = $.el('div', { className: 'board json-index' @@ -10376,7 +10496,7 @@ Index = (function() { return d.title = d.title.replace(/\ -\ Page\ \d+/, ''); }); $.onExists(doc, '.board > .thread > .postContainer, .board + *', function() { - var board, el, len3, m, ref9, timeEl, topNavPos; + var board, el, len3, m, ref8, timeEl, topNavPos; Build.hat = $('.board > .thread > img:first-child'); if (Build.hat) { g.BOARD.threads.forEach(function(thread) { @@ -10395,9 +10515,9 @@ Index = (function() { try { d.implementation.createDocument(null, null, null).appendChild(board); } catch (_error) {} - ref9 = $$('.navLinks'); - for (m = 0, len3 = ref9.length; m < len3; m++) { - el = ref9[m]; + ref8 = $$('.navLinks'); + for (m = 0, len3 = ref8.length; m < len3; m++) { + el = ref8[m]; $.rm(el); } $.rm($.id('ctrl-top')); @@ -10969,57 +11089,42 @@ Index = (function() { return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? '1 hidden thread' : hiddenCount + " hidden threads"; }, update: function(firstTime) { - var now, ref, ref1; - if ((ref = Index.req) != null) { - ref.abort(); + var oldReq; + if ((oldReq = Index.req)) { + delete Index.req; + oldReq.abort(); } - if ((ref1 = Index.notice) != null) { - ref1.close(); - } - if (Conf['Index Refresh Notifications'] && d.readyState !== 'loading') { - Index.notice = new Notice('info', 'Refreshing index...'); + if (Conf['Index Refresh Notifications']) { + Index.notice || (Index.notice = new Notice('info', 'Refreshing index...')); } else { - now = Date.now(); - $.ready(function() { - return Index.nTimeout = setTimeout((function() { - if (Index.req && !Index.notice) { - return Index.notice = new Notice('info', 'Refreshing index...'); - } - }), 3 * $.SECOND - (Date.now() - now)); - }); + Index.nTimeout || (Index.nTimeout = setTimeout(function() { + return Index.notice || (Index.notice = new Notice('info', 'Refreshing index...')); + }, 3 * $.SECOND)); } if (!firstTime && d.readyState !== 'loading' && !$('.board + *')) { location.reload(); return; } - Index.req = $.ajax(location.protocol + "//a.4cdn.org/" + g.BOARD + "/catalog.json", { - onabort: Index.load, - onloadend: Index.load - }, { - whenModified: 'Index', - bypassCache: true - }); + Index.req = $.whenModified(Site.urls.catalogJSON({ + boardID: g.BOARD.ID + }), 'Index', Index.load); return $.addClass(Index.button, 'fa-spin'); }, - load: function(e) { - var err, nTimeout, notice, ref, req, timeEl; + load: function() { + var err, nTimeout, notice, ref, timeEl; + if (this !== Index.req) { + return; + } $.rmClass(Index.button, 'fa-spin'); - req = Index.req, notice = Index.notice, nTimeout = Index.nTimeout; + notice = Index.notice, nTimeout = Index.nTimeout; if (nTimeout) { clearTimeout(nTimeout); } delete Index.nTimeout; delete Index.req; delete Index.notice; - if (e.type === 'abort') { - req.onloadend = null; - if (notice != null) { - notice.close(); - } - return; - } - if ((ref = req.status) !== 200 && ref !== 304) { - err = "Index refresh failed. " + (req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error'); + if ((ref = this.status) !== 200 && ref !== 304) { + err = "Index refresh failed. " + (this.status ? "Error " + this.statusText + " (" + this.status + ")" : 'Connection Error'); if (notice) { notice.setType('warning'); notice.el.lastElementChild.textContent = err; @@ -11030,9 +11135,9 @@ Index = (function() { return; } try { - if (req.status === 200) { - Index.parse(req.response); - } else if (req.status === 304) { + if (this.status === 200) { + Index.parse(this.response); + } else if (this.status === 304) { Index.pageLoad(); } } catch (_error) { @@ -11057,7 +11162,7 @@ Index = (function() { } } timeEl = $('#index-last-refresh time', Index.navLinks); - timeEl.dataset.utc = Date.parse(req.getResponseHeader('Last-Modified')); + timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified')); return RelativeDates.update(timeEl); }, parse: function(pages) { @@ -11910,20 +12015,27 @@ Settings = (function() { return $.after($('input[name="Stubs"]', section).parentNode.parentNode, div); }, "export": function() { - return $.get(Conf, function(Conf) { - delete Conf['boardConfig']; + var Conf2; + Conf2 = {}; + $.extend(Conf2, Conf); + return $.get(Conf2, function(Conf2) { + delete Conf2['boardConfig']; return Settings.downloadExport({ version: g.VERSION, date: Date.now(), - Conf: Conf + Conf: Conf2 }); }); }, downloadExport: function(data) { - var a, p; + var a, blob, p, url; + blob = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json' + }); + url = URL.createObjectURL(blob); a = $.el('a', { download: "4chan X v" + g.VERSION + "-" + data.date + ".json", - href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))))) + href: url }); p = $('.imp-exp-result', Settings.dialog); $.rmAll(p); @@ -12413,7 +12525,7 @@ Settings = (function() { }; }); $.extend(div, { - innerHTML: "
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.

Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.

" + innerHTML: "
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string \`weeaboo\`, case-insensitive.
MD5 and Unique ID filtering use exact string matching, not regular expressions.

Note: If you're using the native catalog rather than 4chan X's catalog, 4chan X's filters do not apply there.
The native catalog has its own separate filter list.

" }); return $('.warning', div).hidden = Conf['Filter']; }, @@ -13120,8 +13232,9 @@ UI = (function() { }; hoverstart = function(arg) { - var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root; - root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove; + var cb, el, endEvents, height, latestEvent, noRemove, o, rect, ref, root, width; + root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, width = arg.width, cb = arg.cb, noRemove = arg.noRemove; + rect = root.getBoundingClientRect(); o = { root: root, el: el, @@ -13133,7 +13246,10 @@ UI = (function() { clientHeight: doc.clientHeight, clientWidth: doc.clientWidth, height: height, - noRemove: noRemove + width: width, + noRemove: noRemove, + clientX: (rect.left + rect.right) / 2, + clientY: (rect.top + rect.bottom) / 2 }; o.hover = hover.bind(o); o.hoverend = hoverend.bind(o); @@ -13161,16 +13277,22 @@ UI = (function() { hoverstart.padding = 25; hover = function(e) { - var clientX, clientY, height, left, ref, right, style, threshold, top; + var clientX, clientY, height, left, marginX, ref, ref1, right, style, threshold, top, width; this.latestEvent = e; height = (this.height || this.el.offsetHeight) + hoverstart.padding; - clientX = e.clientX, clientY = e.clientY; + width = this.width || this.el.offsetWidth; + ref = Conf['Follow Cursor'] ? e : this, clientX = ref.clientX, clientY = ref.clientY; top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120)); threshold = this.clientWidth / 2; if (!this.isImage) { threshold = Math.max(threshold, this.clientWidth - 400); } - ref = clientX <= threshold ? [clientX + 45 + 'px', ''] : ['', this.clientWidth - clientX + 45 + 'px'], left = ref[0], right = ref[1]; + marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45; + if (this.isImage) { + marginX = Math.min(marginX, this.clientWidth - width); + } + marginX += 'px'; + ref1 = clientX <= threshold ? [marginX, ''] : ['', marginX], left = ref1[0], right = ref1[1]; style = this.style; style.top = top + 'px'; style.left = left; @@ -13879,8 +14001,11 @@ ImageCommon = (function() { return cb(URL); } }; - return $.ajax(location.protocol + "//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", { - onload: function() { + return $.ajax(Site.urls.threadJSON({ + boardID: post.boardID, + threadID: post.threadID + }), { + onloadend: function() { var i, len, postObj, ref; if (this.status === 404) { post.kill(!post.isClone); @@ -14454,7 +14579,7 @@ ImageHover = (function() { }, mouseover: function(post) { return function(e) { - var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x; + var el, error, file, height, isVideo, maxHeight, maxWidth, ref, ref1, scale, width, x; if (!doc.contains(this)) { return; } @@ -14490,28 +14615,32 @@ ImageHover = (function() { } } } - ref1 = (function() { - var i, len, ref1, results; - ref1 = file.dimensions.split('x'); - results = []; - for (i = 0, len = ref1.length; i < len; i++) { - x = ref1[i]; - results.push(+x); - } - return results; - })(), width = ref1[0], height = ref1[1]; - ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right; - maxWidth = Math.max(left, doc.clientWidth - right); - maxHeight = doc.clientHeight - UI.hover.padding; - scale = Math.min(1, maxWidth / width, maxHeight / height); - el.style.maxWidth = (scale * width) + "px"; - el.style.maxHeight = (scale * height) + "px"; + if (file.dimensions) { + ref1 = (function() { + var i, len, ref1, results; + ref1 = file.dimensions.split('x'); + results = []; + for (i = 0, len = ref1.length; i < len; i++) { + x = ref1[i]; + results.push(+x); + } + return results; + })(), width = ref1[0], height = ref1[1]; + maxWidth = doc.clientWidth; + maxHeight = doc.clientHeight - UI.hover.padding; + scale = Math.min(1, maxWidth / width, maxHeight / height); + width *= scale; + height *= scale; + el.style.maxWidth = width + "px"; + el.style.maxHeight = height + "px"; + } return UI.hover({ root: this, el: el, latestEvent: e, endEvents: 'mouseout click', - height: scale * height, + height: height, + width: width, noRemove: true, cb: function() { $.off(el, 'error', error); @@ -15369,7 +15498,7 @@ Embedding = (function() { return Embedding.flushTitles(service); } } else { - return CrossOrigin.json(service.api(uid), (function() { + return CrossOrigin.cache(service.api(uid), (function() { return Embedding.cb.title(this, data); })); } @@ -15388,7 +15517,7 @@ Embedding = (function() { Embedding.cb.title(this, data); } }; - return CrossOrigin.json(service.api((function() { + return CrossOrigin.cache(service.api((function() { var j, len, results; results = []; for (j = 0, len = queue.length; j < len; j++) { @@ -15610,7 +15739,7 @@ Embedding = (function() { hidden: true, id: "gist-embed-" + (counter++) }); - CrossOrigin.json("https://api.github.com/gists/" + a.dataset.uid, function() { + CrossOrigin.cache("https://api.github.com/gists/" + a.dataset.uid, function() { el.textContent = Object.values(this.response.files)[0].content; el.className = 'prettyprint'; $.global(function() { @@ -16341,18 +16470,21 @@ DeleteLink = (function() { return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), { responseType: 'document', withCredentials: true, - onload: function() { + onloadend: function() { return DeleteLink.load(link, post, fileOnly, this.response); }, - onerror: function() { - return DeleteLink.error(link, post); - } - }, { form: $.formData(form) }); }, load: function(link, post, fileOnly, resDoc) { var el, msg; + if (!resDoc) { + new Notice('warning', 'Connection error, please retry.', 20); + if (post.fullID === DeleteLink.post.fullID) { + $.on(link, 'click', DeleteLink.toggle); + } + return; + } link.textContent = DeleteLink.linkText(fileOnly); if (resDoc.title === '4chan - Banned') { el = $.el('span', { @@ -16381,12 +16513,6 @@ DeleteLink = (function() { } } }, - error: function(link, post) { - new Notice('warning', 'Connection error, please retry.', 20); - if (post.fullID === DeleteLink.post.fullID) { - return $.on(link, 'click', DeleteLink.toggle); - } - }, cooldown: { seconds: {}, start: function(post, seconds) { @@ -16986,7 +17112,10 @@ ExpandComment = (function() { return; } a.textContent = "Post No." + post + " Loading..."; - return $.cache(location.protocol + "//a.4cdn.org" + (a.pathname.split(/\/+/).splice(0, 4).join('/')) + ".json", function() { + return $.cache(Site.urls.threadJSON({ + boardID: post.boardID, + threadID: post.threadID + }), function() { return ExpandComment.parse(this, a, post); }); }, @@ -17004,7 +17133,7 @@ ExpandComment = (function() { var callback, clone, comment, href, i, j, k, len, len1, len2, postObj, posts, quote, ref, ref1, spoilerRange, status; status = req.status; if (status !== 200 && status !== 304) { - a.textContent = "Error " + req.statusText + " (" + status + ")"; + a.textContent = status ? "Error " + req.statusText + " (" + status + ")" : 'Connection Error'; return; } posts = req.response.posts; @@ -17085,15 +17214,16 @@ ExpandThread = (function() { return $.on(a, 'click', ExpandThread.cbToggle); }, disconnect: function(refresh) { - var ref, ref1, status, threadID; + var oldReq, ref, status, threadID; if (g.VIEW === 'thread' || !Conf['Thread Expansion']) { return; } ref = ExpandThread.statuses; for (threadID in ref) { status = ref[threadID]; - if ((ref1 = status.req) != null) { - ref1.abort(); + if ((oldReq = status.req)) { + delete status.req; + oldReq.abort(); } delete ExpandThread.statuses[threadID]; } @@ -17141,17 +17271,24 @@ ExpandThread = (function() { var status; ExpandThread.statuses[thread] = status = {}; a.textContent = Build.summaryText.apply(Build, ['...'].concat(slice.call(a.textContent.match(/\d+/g)))); - return status.req = $.cache(location.protocol + "//a.4cdn.org/" + thread.board + "/thread/" + thread + ".json", function() { + return status.req = $.cache(Site.urls.threadJSON({ + boardID: thread.board.ID, + threadID: thread.ID + }), function() { + if (this !== status.req) { + return; + } delete status.req; return ExpandThread.parse(this, thread, a); }); }, contract: function(thread, a, threadRoot) { - var filesCount, i, inlined, len, num, postsCount, replies, reply, status; + var filesCount, i, inlined, len, num, oldReq, postsCount, replies, reply, status; status = ExpandThread.statuses[thread]; delete ExpandThread.statuses[thread]; - if (status.req) { - status.req.abort(); + if ((oldReq = status.req)) { + delete status.req; + oldReq.abort(); if (a) { a.textContent = Build.summaryText.apply(Build, ['+'].concat(slice.call(a.textContent.match(/\d+/g)))); } @@ -17201,7 +17338,7 @@ ExpandThread = (function() { parse: function(req, thread, a) { var a2, filesCount, i, len, post, postData, posts, postsCount, postsRoot, ref, ref1, root; if ((ref = req.status) !== 200 && ref !== 304) { - a.textContent = "Error " + req.statusText + " (" + req.status + ")"; + a.textContent = req.status ? "Error " + req.statusText + " (" + req.status + ")" : 'Connection Error'; return; } Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler; @@ -18736,7 +18873,6 @@ Report = (function() { results = []; fn = function(name, url) { return $.ajax(url, { - responseType: 'json', onloadend: function() { results.push([ name, this.response || { @@ -18746,8 +18882,7 @@ Report = (function() { if (results.length === urls.length) { return cb(results); } - } - }, { + }, form: form }); }; @@ -19379,12 +19514,9 @@ ThreadStats = (function() { return; } ThreadStats.timeout = setTimeout(ThreadStats.fetchPage, 2 * $.MINUTE); - return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadStats.thread.board + "/threads.json", { - onload: ThreadStats.onThreadsLoad - }, { - whenModified: 'ThreadStats', - bypassCache: true - }); + return $.whenModified(Site.urls.threadsListJSON({ + boardID: ThreadStats.thread.board + }), 'ThreadStats', ThreadStats.onThreadsLoad); }, onThreadsLoad: function() { var i, j, k, len, len1, len2, page, purgePos, ref, ref1, ref2, thread; @@ -19593,11 +19725,12 @@ ThreadUpdater = (function() { } }, load: function() { - var req; - req = ThreadUpdater.req; - switch (req.status) { + if (this !== ThreadUpdater.req) { + return; + } + switch (this.status) { case 200: - ThreadUpdater.parse(req); + ThreadUpdater.parse(this); if (ThreadUpdater.thread.isArchived) { return ThreadUpdater.kill(); } else { @@ -19605,7 +19738,9 @@ ThreadUpdater = (function() { } break; case 404: - return $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/catalog.json", { + return $.ajax(Site.urls.catalogJSON({ + boardID: ThreadUpdater.thread.board.ID + }), { onloadend: function() { var confirmed, i, k, len, len1, page, ref, ref1, thread; if (this.status === 200) { @@ -19628,12 +19763,12 @@ ThreadUpdater = (function() { if (confirmed) { return ThreadUpdater.kill(); } else { - return ThreadUpdater.error(req); + return ThreadUpdater.error(this); } } }); default: - return ThreadUpdater.error(req); + return ThreadUpdater.error(this); } } }, @@ -19711,18 +19846,18 @@ ThreadUpdater = (function() { return ThreadUpdater.seconds--; }, update: function() { - var ref; + var oldReq; clearTimeout(ThreadUpdater.timeoutID); ThreadUpdater.set('timer', '...', 'loading'); - if ((ref = ThreadUpdater.req) != null) { - ref.abort(); + if ((oldReq = ThreadUpdater.req)) { + delete ThreadUpdater.req; + oldReq.abort(); } - return ThreadUpdater.req = $.ajax(location.protocol + "//a.4cdn.org/" + ThreadUpdater.thread.board + "/thread/" + ThreadUpdater.thread + ".json", { - onloadend: ThreadUpdater.cb.load, + return ThreadUpdater.req = $.whenModified(Site.urls.threadJSON({ + boardID: ThreadUpdater.thread.board.ID, + threadID: ThreadUpdater.thread.ID + }), 'ThreadUpdater', ThreadUpdater.cb.load, { timeout: $.MINUTE - }, { - whenModified: 'ThreadUpdater', - bypassCache: true }); }, updateThreadStatus: function(type, status) { @@ -19882,6 +20017,7 @@ ThreadWatcher = (function() { className: 'fa fa-eye' }); this.db = new DataBoard('watchedThreads', this.refresh, true); + this.dbLM = new DataBoard('watcherLastModified', null, true); this.dialog = UI.dialog('thread-watcher', { innerHTML: "
Thread Watcher ×
" }); @@ -19912,6 +20048,7 @@ ThreadWatcher = (function() { this.dialog.hidden = true; } Header.addShortcut('watcher', sc, 510); + ThreadWatcher.initLastModified(); ThreadWatcher.fetchAuto(); $.on(window, 'visibilitychange focus', function() { return $.queueTask(ThreadWatcher.fetchAuto); @@ -20148,6 +20285,38 @@ ThreadWatcher = (function() { }, requests: [], fetched: 0, + fetch: function(url, arg, args, cb) { + var ajax, force, onloadend, ref, req, siteID; + siteID = arg.siteID, force = arg.force; + if (ThreadWatcher.requests.length === 0) { + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + } + onloadend = function() { + if (this.finished) { + return; + } + this.finished = true; + ThreadWatcher.fetched++; + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + ThreadWatcher.clearRequests(); + } else { + ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%"; + } + return cb.apply(this, args); + }; + ajax = siteID === Site.hostname ? $.ajax : CrossOrigin.ajax; + if (force) { + if ((ref = $.lastModified.ThreadWatcher) != null) { + delete ref[url]; + } + } + req = $.whenModified(url, 'ThreadWatcher', onloadend, { + timeout: $.MINUTE, + ajax: ajax + }); + return ThreadWatcher.requests.push(req); + }, clearRequests: function() { ThreadWatcher.requests = []; ThreadWatcher.fetched = 0; @@ -20156,15 +20325,44 @@ ThreadWatcher = (function() { }, abort: function() { var i, len1, ref, req; + delete ThreadWatcher.syncing; ref = ThreadWatcher.requests; for (i = 0, len1 = ref.length; i < len1; i++) { req = ref[i]; - if (req.readyState !== 4) { - req.abort(); + if (!(!req.finished)) { + continue; } + req.finished = true; + req.abort(); } return ThreadWatcher.clearRequests(); }, + initLastModified: function() { + var base, boardID, boards, data, date, lm, ref, ref1, siteID, url; + lm = ((base = $.lastModified)['ThreadWatcher'] || (base['ThreadWatcher'] = {})); + ref = ThreadWatcher.dbLM.data; + for (siteID in ref) { + boards = ref[siteID]; + ref1 = boards.boards; + for (boardID in ref1) { + data = ref1[boardID]; + if (ThreadWatcher.db.get({ + siteID: siteID, + boardID: boardID + })) { + for (url in data) { + date = data[url]; + lm[url] = date; + } + } else { + ThreadWatcher.dbLM["delete"]({ + siteID: siteID, + boardID: boardID + }); + } + } + } + }, fetchAuto: function() { var db, interval, now, ref; clearTimeout(ThreadWatcher.timeout); @@ -20176,41 +20374,123 @@ ThreadWatcher = (function() { now = Date.now(); if (!((now - interval < (ref = db.data.lastChecked || 0) && ref <= now) || d.hidden || !d.hasFocus())) { ThreadWatcher.fetchAllStatus(); - db.setLastChecked(); } return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval); }, buttonFetchAll: function() { - if (ThreadWatcher.requests.length) { + if (ThreadWatcher.syncing || ThreadWatcher.requests.length) { return ThreadWatcher.abort(); } else { return ThreadWatcher.fetchAllStatus(); } }, fetchAllStatus: function() { - var db, dbs, i, len1, n, results; + var dbi, dbs, i, len1, n, results; + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + ThreadWatcher.syncing = true; dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(function(x) { return x; }); n = 0; results = []; for (i = 0, len1 = dbs.length; i < len1; i++) { - db = dbs[i]; - results.push(db.forceSync(function() { - var j, len2, thread, threads; + dbi = dbs[i]; + results.push(dbi.forceSync(function() { + var board, boards, db, deep, j, len2, now, ref; if ((++n) === dbs.length) { - threads = ThreadWatcher.getAll(); - for (j = 0, len2 = threads.length; j < len2; j++) { - thread = threads[j]; - ThreadWatcher.fetchStatus(thread); + if (!ThreadWatcher.syncing) { + return; + } + delete ThreadWatcher.syncing; + db = ThreadWatcher.db; + now = Date.now(); + deep = !((now - 2 * $.HOUR < (ref = db.data.lastChecked2 || 0) && ref <= now)); + boards = ThreadWatcher.getAll(true); + for (j = 0, len2 = boards.length; j < len2; j++) { + board = boards[j]; + ThreadWatcher.fetchBoard(board, deep); + } + db.setLastChecked(); + if (deep) { + db.setLastChecked('lastChecked2'); + } + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + return ThreadWatcher.clearRequests(); } } })); } return results; }, + fetchBoard: function(board, deep) { + var base, boardID, ref, ref1, ref2, siteID, software, url, urlF; + if (!board.some(function(thread) { + return !thread.data.isDead; + })) { + return; + } + ref = board[0], siteID = ref.siteID, boardID = ref.boardID; + software = (ref1 = Conf['siteProperties'][siteID]) != null ? ref1.software : void 0; + urlF = deep && software === 'tinyboard' ? 'catalogJSON' : 'threadsListJSON'; + url = (ref2 = SW[software]) != null ? typeof (base = ref2.urls)[urlF] === "function" ? base[urlF]({ + siteID: siteID, + boardID: boardID + }) : void 0 : void 0; + if (!url) { + return; + } + return ThreadWatcher.fetch(url, { + siteID: siteID + }, [board, url], ThreadWatcher.parseBoard); + }, + parseBoard: function(board, url) { + var boardID, data, i, item, j, k, len1, len2, len3, lmDate, modified, page, ref, ref1, ref2, replies, siteID, thread, threadID; + if (this.status !== 200) { + return; + } + ref = board[0], siteID = ref.siteID, boardID = ref.boardID; + lmDate = this.getResponseHeader('Last-Modified'); + ThreadWatcher.dbLM.extend({ + siteID: siteID, + boardID: boardID, + val: $.item(url, lmDate) + }); + modified = {}; + replies = {}; + try { + ref1 = this.response; + for (i = 0, len1 = ref1.length; i < len1; i++) { + page = ref1[i]; + ref2 = page.threads; + for (j = 0, len2 = ref2.length; j < len2; j++) { + item = ref2[j]; + modified[item.no] = item.last_modified; + replies[item.no] = item.replies; + } + } + } catch (_error) {} + for (k = 0, len3 = board.length; k < len3; k++) { + thread = board[k]; + threadID = thread.threadID, data = thread.data; + if (modified[threadID]) { + if (modified[threadID] === data.modified && ((replies[threadID] == null) || replies[threadID] === data.replies)) { + continue; + } + ThreadWatcher.db.extend({ + siteID: siteID, + boardID: boardID, + threadID: threadID, + val: { + modified: modified[threadID] + } + }); + } + ThreadWatcher.fetchStatus(thread); + } + }, fetchStatus: function(thread, force) { - var base, boardID, data, ref, ref1, req, siteID, software, threadID, url; + var base, boardID, data, ref, ref1, siteID, software, threadID, url; siteID = thread.siteID, boardID = thread.boardID, threadID = thread.threadID, data = thread.data; software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0; url = (ref1 = SW[software]) != null ? typeof (base = ref1.urls).threadJSON === "function" ? base.threadJSON({ @@ -20227,46 +20507,18 @@ ThreadWatcher = (function() { if (data.last === -1) { return; } - if (ThreadWatcher.requests.length === 0) { - ThreadWatcher.status.textContent = '...'; - $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); - } - if ((typeof Site.hasCORS === "function" ? Site.hasCORS(url) : void 0) || url.split('/').slice(0, 3).join('/') === location.origin) { - req = $.ajax(url, { - onloadend: function() { - return ThreadWatcher.parseStatus.call(this, thread); - }, - timeout: $.MINUTE - }, { - whenModified: force ? false : 'ThreadWatcher' - }); - } else { - req = { - abort: function() { - return req.aborted = true; - } - }; - CrossOrigin.json(url, function() { - if (req.aborted) { - return; - } - return ThreadWatcher.parseStatus.call(this, thread); - }, true, $.MINUTE); - } - return ThreadWatcher.requests.push(req); + return ThreadWatcher.fetch(url, { + siteID: siteID, + force: force + }, [thread], ThreadWatcher.parseStatus); }, parseStatus: function(arg) { - var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, siteID, software, threadID, unread, updated, youOP; + var boardID, data, i, isDead, last, lastReadPost, len1, match, postObj, quotesYou, quotingYou, ref, ref1, ref2, ref3, regexp, replies, siteID, software, threadID, unread, updated, youOP; siteID = arg.siteID, boardID = arg.boardID, threadID = arg.threadID, data = arg.data; - ThreadWatcher.fetched++; - if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { - ThreadWatcher.clearRequests(); - } else { - ThreadWatcher.status.textContent = (Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)) + "%"; - } software = (ref = Conf['siteProperties'][siteID]) != null ? ref.software : void 0; if (this.status === 200 && this.response) { last = this.response.posts[this.response.posts.length - 1].no; + replies = this.response.posts.length - 1; isDead = !!this.response.posts[0].archived; if (isDead && Conf['Auto Prune']) { ThreadWatcher.db["delete"]({ @@ -20341,6 +20593,7 @@ ThreadWatcher = (function() { threadID: threadID, val: { last: last, + replies: replies, isDead: isDead, unread: unread, quotingYou: quotingYou @@ -20380,8 +20633,8 @@ ThreadWatcher = (function() { return ThreadWatcher.refresh(); } }, - getAll: function() { - var all, boardID, boards, data, ref, ref1, siteID, threadID, threads; + getAll: function(groupByBoard) { + var all, boardID, boards, cont, data, ref, ref1, siteID, threadID, threads; all = []; ref = ThreadWatcher.db.data; for (siteID in ref) { @@ -20392,10 +20645,13 @@ ThreadWatcher = (function() { if (Conf['Current Board'] && (siteID !== Site.hostname || boardID !== g.BOARD.ID)) { continue; } + if (groupByBoard) { + all.push((cont = [])); + } for (threadID in threads) { data = threads[threadID]; if (data && typeof data === 'object') { - all.push({ + (groupByBoard ? cont : all).push({ siteID: siteID, boardID: boardID, threadID: threadID, @@ -20949,12 +21205,15 @@ Unread = (function() { return; } }, - openNotification: function(post) { + openNotification: function(post, predicate) { var notif; + if (predicate == null) { + predicate = ' replied to you'; + } if (!Header.areNotificationsEnabled) { return; } - notif = new Notification(post.info.nameBlock + " replied to you", { + notif = new Notification("" + post.info.nameBlock + predicate, { body: post.commentDisplay(), icon: Favicon.logo }); @@ -22730,7 +22989,7 @@ QR = (function() { } }, submit: function(e) { - var captcha, cb, err, extra, filetag, formData, options, post, ref, thread, threadID; + var captcha, cb, err, filetag, formData, options, post, ref, thread, threadID; if (e != null) { e.preventDefault(); } @@ -22800,47 +23059,35 @@ QR = (function() { options = { responseType: 'document', withCredentials: true, - onload: QR.response, - onerror: function() { - delete QR.req; - if (QR.currentCaptcha) { - Captcha.cache.save(QR.currentCaptcha); - } - delete QR.currentCaptcha; - post.unlock(); - QR.cooldown.auto = true; - QR.cooldown.addDelay(post, 2); - QR.status(); - return QR.error(QR.connectionError()); - } - }; - extra = { + onloadend: QR.response, form: $.formData(formData) }; if (Conf['Show Upload Progress']) { - extra.upCallbacks = { - onload: function() { + options.onprogress = function(e) { + var ref1; + if (this !== ((ref1 = QR.req) != null ? ref1.upload : void 0)) { + return; + } + if (e.loaded < e.total) { + QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%"; + } else { QR.req.isUploadFinished = true; QR.req.progress = '...'; - return QR.status(); - }, - onprogress: function(e) { - QR.req.progress = (Math.round(e.loaded / e.total * 100)) + "%"; - return QR.status(); } + return QR.status(); }; } cb = function(response) { if (response != null) { QR.currentCaptcha = response; if (response.challenge != null) { - extra.form.append('recaptcha_challenge_field', response.challenge); - extra.form.append('recaptcha_response_field', response.response); + options.form.append('recaptcha_challenge_field', response.challenge); + options.form.append('recaptcha_response_field', response.response); } else { - extra.form.append('g-recaptcha-response', response.response); + options.form.append('g-recaptcha-response', response.response); } } - QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options, extra); + QR.req = $.ajax("https://sys." + (location.hostname.split('.')[1]) + ".org/" + g.BOARD + "/post", options); return QR.req.progress = '...'; }; if (typeof captcha === 'function') { @@ -22866,23 +23113,24 @@ QR = (function() { return QR.status(); }, response: function() { - var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, req, resDoc, seconds, threadID; - req = QR.req; + var URL, _, connErr, err, h1, isReply, lastPostToThread, m, open, post, postID, postsCount, ref, ref1, ref2, ref3, seconds, threadID; + if (this !== QR.req) { + return; + } delete QR.req; post = QR.posts[0]; post.unlock(); - resDoc = req.response; - if ((err = resDoc.getElementById('errmsg'))) { - if ((ref = $('a', err)) != null) { - ref.target = '_blank'; + if ((err = (ref = this.response) != null ? ref.getElementById('errmsg') : void 0)) { + if ((ref1 = $('a', err)) != null) { + ref1.target = '_blank'; } - } else if ((connErr = resDoc.title !== 'Post successful!')) { + } else if ((connErr = !this.response || this.response.title !== 'Post successful!')) { err = QR.connectionError(); if (QR.currentCaptcha) { Captcha.cache.save(QR.currentCaptcha); } - } else if (req.status !== 200) { - err = "Error " + req.statusText + " (" + req.status + ")"; + } else if (this.status !== 200) { + err = "Error " + this.statusText + " (" + this.status + ")"; } delete QR.currentCaptcha; if (err) { @@ -22905,13 +23153,13 @@ QR = (function() { } else { QR.cooldown.auto = false; } - QR.captcha.setup(QR.cooldown.auto && ((ref1 = d.activeElement) === QR.nodes.status || ref1 === d.body)); + QR.captcha.setup(QR.cooldown.auto && ((ref2 = d.activeElement) === QR.nodes.status || ref2 === d.body)); QR.status(); QR.error(err); return; } - h1 = $('h1', resDoc); - ref2 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref2[0], threadID = ref2[1], postID = ref2[2]; + h1 = $('h1', this.response); + ref3 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = ref3[0], threadID = ref3[1], postID = ref3[2]; postID = +postID; threadID = +threadID || postID; isReply = threadID !== postID; @@ -22928,10 +23176,10 @@ QR = (function() { postsCount = QR.posts.length - 1; QR.cooldown.auto = postsCount && isReply; lastPostToThread = !((function() { - var j, len, p, ref3; - ref3 = QR.posts.slice(1); - for (j = 0, len = ref3.length; j < len; j++) { - p = ref3[j]; + var j, len, p, ref4; + ref4 = QR.posts.slice(1); + for (j = 0, len = ref4.length; j < len; j++) { + p = ref4[j]; if (p.thread === post.thread) { return true; } @@ -22982,17 +23230,18 @@ QR = (function() { } else { return setTimeout(check, attempts * $.SECOND); } - } - }, { + }, + responseType: 'text', type: 'HEAD' }); }; return check(); }, abort: function() { - if (QR.req && !QR.req.isUploadFinished) { - QR.req.abort(); + var oldReq; + if ((oldReq = QR.req) && !QR.req.isUploadFinished) { delete QR.req; + oldReq.abort(); if (QR.currentCaptcha) { Captcha.cache.save(QR.currentCaptcha); } diff --git a/builds/4chan-X.zip b/builds/4chan-X.zip index f248742d4..e72ed6cf1 100644 Binary files a/builds/4chan-X.zip and b/builds/4chan-X.zip differ diff --git a/builds/updates-beta.json b/builds/updates-beta.json index 648b776f9..7fc266478 100644 --- a/builds/updates-beta.json +++ b/builds/updates-beta.json @@ -3,7 +3,7 @@ "4chan-x@4chan-x.net": { "updates": [ { - "version": "1.14.5.14", + "version": "1.14.6.0", "update_link": "https://www.4chan-x.net/builds/4chan-X-beta.crx" } ] diff --git a/builds/updates-beta.xml b/builds/updates-beta.xml index 28c92cb5b..ffff4e9a9 100644 --- a/builds/updates-beta.xml +++ b/builds/updates-beta.xml @@ -1,7 +1,7 @@ - + diff --git a/builds/updates.json b/builds/updates.json index faa9a6b39..cb815a87a 100644 --- a/builds/updates.json +++ b/builds/updates.json @@ -3,7 +3,7 @@ "4chan-x@4chan-x.net": { "updates": [ { - "version": "1.14.5.14", + "version": "1.14.6.0", "update_link": "https://www.4chan-x.net/builds/4chan-X.crx" } ] diff --git a/builds/updates.xml b/builds/updates.xml index e88c7707d..dc138395e 100644 --- a/builds/updates.xml +++ b/builds/updates.xml @@ -1,7 +1,7 @@ - + diff --git a/version.json b/version.json index 5105f37ec..ffc5b9ba4 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "version": "1.14.5.14", - "date": "2019-03-22T09:53:32.789Z" + "version": "1.14.6.0", + "date": "2019-03-25T08:01:52.363Z" } \ No newline at end of file