diff --git a/CHANGELOG.md b/CHANGELOG.md index 1db3b199e..eb6637b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -# 3.0.0 +### 3.0.1 - *2013-04-08* + +- Added the possibility to combine board-list toggle and custom text. +- Added Reply Navigation back in, disabled by default. +- Fixed Thread Hiding initialization error. + +# 3.0.0 - *2013-04-07* **Major rewrite of 4chan X.** @@ -7,10 +13,16 @@ Header: - The board list can be customized. - The Header can be automatically hidden. +Extension-related changes for Chrome and Opera: + - Installing and updating is now pain-free on Chrome. + - Settings will persist on different subdomains and protocols (HTTP/HTTPS). + - Settings will persist in Incognito on Chrome. + - Clearing your cookies won't erase your settings anymore. + - Fixed Chrome's install warning saying that 4chan X would run on all web sites. + Egocentrism: - `(You)` will be added to quotes linking to your posts. - The Unread tab icon will indicate new unread posts quoting you with an exclamation mark. - - Delete links in the post menu will only appear for your posts. Quick Reply changes: - Opening text files will insert their content in the comment field. @@ -21,11 +33,12 @@ Quick Reply changes: - Closing the QR while uploading will abort the upload and won't close the QR anymore. - Creating threads outside of the index is now possible. - Selection-to-quote also applies to selected text inside the post, not just inside the comment. + - Added support for thread creation in the catalog. - Added thumbnailing support for Opera. Image Expansion changes: - The toggle and settings are now located in the Header's shortcuts and menu. - - There is now a setting to allow expanding spoilers. + - Expanding spoilers along with all non-spoiler images is now optional, and disabled by default. - Expanding OP images won't squish replies anymore. Thread Updater changes: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66cdd1973..685ca0727 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ 1. Make sure both your **browser** and **4chan X** are up to date. 2. Disable your other extensions & scripts to identify conflicts. 3. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information: - 1. Precise steps to reproduce the problem. + 1. Precise steps to reproduce the problem, with the expected and actual results. 2. Console errors, if any. 3. Browser version. 4. Your exported settings. @@ -21,7 +21,7 @@ Open your console with: - Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`. - Clone 4chan X. - `cd` into it. -- Install 4chan X dependencies with `npm install`. +- Install/Update 4chan X dependencies with `npm install`. ### Build diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 40e433381..7888933c0 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -20,6 +20,7 @@ module.exports = (grunt) -> 'src/features.coffee' 'src/qr.coffee' 'src/report.coffee' + 'src/databoard.coffee' 'src/main.coffee' ] dest: 'tmp/script.coffee' @@ -75,10 +76,10 @@ module.exports = (grunt) -> command: -> release = "#{pkg.meta.name} v#{pkg.version}" return [ - "git checkout #{pkg.meta.mainBranch}" - "git commit -am 'Release #{release}.'" - "git tag -a #{pkg.version} -m '#{release}.'" - "git tag -af stable -m '#{release}.'" + 'git checkout ' + pkg.meta.mainBranch, + 'git commit -am "Release ' + release + '."', + 'git tag -a ' + pkg.version + ' -m "' + release + '."', + 'git tag -af stable-v3 -m "' + release + '."' ].join(' && '); stdout: true diff --git a/appchan-x.user.js b/appchan-x.user.js index 218a297a1..72389fb2a 100644 --- a/appchan-x.user.js +++ b/appchan-x.user.js @@ -20,7 +20,7 @@ // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAgMAAAAqbBEUAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAHFJREFUKFOt0LENACEIBdBv4Qju4wgWanEj3D6OcIVMKaitYHEU/jwTCQj8W75kiVCSBvdQ5/AvfVHBin11BgdRq3ysBgfwBDRrj3MCIA+oAQaku/Q1cNctrAmyDl577tOThYt/Y1RBM4DgOHzM0HFTAyLukH/cmRnqAAAAAElFTkSuQmCC // ==/UserScript== -/* appchan x - Version 2.0.0 - 2013-03-28 +/* appchan x - Version 2.0.0 - 2013-04-08 * http://zixaphir.github.com/appchan-x/ * * Copyright (c) 2009-2011 James Campos @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, CatalogLinks, Clone, Conf, Config, CustomCSS, DeleteLink, DownloadLink, Emoji, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Fourchan, Get, GlobalMessage, Header, Icons, ImageExpand, ImageHover, ImageReplace, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Misc, Nav, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DataBoards, DeleteLink, DownloadLink, Emoji, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Fourchan, Get, GlobalMessage, Header, Icons, ImageExpand, ImageHover, ImageReplace, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Nav, Notification, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -63,6 +63,7 @@ 'Comment Expansion': [true, 'Add buttons to expand long comments.'], 'Thread Expansion': [true, 'Add buttons to expand threads.'], 'Index Navigation': [false, 'Add buttons to navigate between threads.'], + 'Reply Navigation': [false, 'Add buttons to navigate to top / bottom of thread.'], 'Check for Updates': [true, 'Check for updated versions of appchan x.'] }, 'Linkification': { @@ -119,7 +120,7 @@ }, 'Quote Links': { 'Quote Backlinks': [true, 'Add quote backlinks.'], - 'OP Backlinks': [false, 'Add backlinks to the OP.'], + 'OP Backlinks': [true, 'Add backlinks to the OP.'], 'Quote Inlining': [true, 'Inline quoted post on click.'], 'Forward Hiding': [true, 'Hide original posts of inlined backlinks.'], 'Quote Previewing': [true, 'Show quoted post on hover.'], @@ -239,7 +240,7 @@ filesize: '', MD5: '' }, - sauces: "http://iqdb.org/?url=%TURL\nhttps://www.google.com/searchbyimage?image_url=%TURL\n#//tineye.com/search?url=%TURL\n#http://saucenao.com/search.php?url=%TURL\n#http://3d.iqdb.org/?url=%TURL\n#http://regex.info/exif.cgi?imgurl=%URL\n# uploaders:\n#http://imgur.com/upload?url=%URL;text:Upload to imgur\n#http://ompldr.org/upload?url1=%URL;text:Upload to ompldr\n# \"View Same\" in archives:\n#//archive.foolz.us/_/search/image/%MD5/;text:View same on foolz\n#//archive.foolz.us/%board/search/image/%MD5/;text:View same on foolz /%board/\n#//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/", + sauces: "https://www.google.com/searchbyimage?image_url=%TURL\nhttp://iqdb.org/?url=%TURL\n#//tineye.com/search?url=%TURL\n#http://saucenao.com/search.php?url=%TURL\n#http://3d.iqdb.org/?url=%TURL\n#http://regex.info/exif.cgi?imgurl=%URL\n# uploaders:\n#http://imgur.com/upload?url=%URL;text:Upload to imgur\n#http://ompldr.org/upload?url1=%URL;text:Upload to ompldr\n# \"View Same\" in archives:\n#//archive.foolz.us/_/search/image/%MD5/;text:View same on foolz\n#//archive.foolz.us/%board/search/image/%MD5/;text:View same on foolz /%board/\n#//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/", 'Custom CSS': false, 'Header auto-hide': false, 'Header catalog links': false, @@ -2412,14 +2413,14 @@ dialog = function(id, position, html) { var el, move; - el = d.createElement('div'); - el.className = 'dialog'; - el.innerHTML = html; - el.id = id; + el = $.el('div', { + className: 'dialog', + innerHTML: html, + id: id + }); el.style.cssText = localStorage.getItem("" + g.NAMESPACE + id + ".position") || position; - move = el.querySelector('.move'); - move.addEventListener('touchstart', dragstart, false); - move.addEventListener('mousedown', dragstart, false); + move = $('.move', el); + $.on(move, 'touchstart mousedown', dragstart); return el; }; Menu = (function() { @@ -2534,8 +2535,7 @@ $.rm(currentMenu); currentMenu = null; lastToggledButton = null; - $.off(d, 'click', this.close); - return $.off(d, 'CloseMenu', this.close); + return $.off(d, 'click CloseMenu', this.close); }; Menu.prototype.findNextEntry = function(entry, direction) { @@ -2595,7 +2595,7 @@ }; Menu.prototype.focus = function(entry) { - var bottom, cHeight, cWidth, eRect, focused, left, right, sRect, style, submenu, top, _i, _len, _ref; + var bottom, cHeight, cWidth, eRect, focused, left, right, sRect, style, submenu, top, _i, _len, _ref, _ref1, _ref2; while (focused = $.x('parent::*/child::*[contains(@class,"focused")]', entry)) { $.rmClass(focused, 'focused'); @@ -2613,20 +2613,8 @@ eRect = entry.getBoundingClientRect(); cHeight = doc.clientHeight; cWidth = doc.clientWidth; - if (eRect.top + sRect.height < cHeight) { - top = '0px'; - bottom = 'auto'; - } else { - top = 'auto'; - bottom = '0px'; - } - if (eRect.right + sRect.width < cWidth) { - left = '100%'; - right = 'auto'; - } else { - left = 'auto'; - right = '100%'; - } + _ref1 = eRect.top + sRect.height < cHeight ? ['0px', 'auto'] : ['auto', '0px'], top = _ref1[0], bottom = _ref1[1]; + _ref2 = eRect.right + sRect.width < cWidth ? ['100%', 'auto'] : ['auto', '100%'], left = _ref2[0], right = _ref2[1]; style = submenu.style; style.top = top; style.bottom = bottom; @@ -2676,10 +2664,10 @@ return; } e.preventDefault(); - el = $.x('ancestor::div[contains(@class,"dialog")][1]', this); if (isTouching = e.type === 'touchstart') { e = e.changedTouches[e.changedTouches.length - 1]; } + el = $.x('ancestor::div[contains(@class,"dialog")][1]', this); rect = el.getBoundingClientRect(); screenHeight = doc.clientHeight; screenWidth = doc.clientWidth; @@ -2698,14 +2686,13 @@ o.identifier = e.identifier; o.move = touchmove.bind(o); o.up = touchend.bind(o); - d.addEventListener('touchmove', o.move, false); - d.addEventListener('touchend', o.up, false); - return d.addEventListener('touchcancel', o.up, false); + $.on(d, 'touchmove', o.move); + return $.on(d, 'touchend touchcancel', o.up); } else { o.move = drag.bind(o); o.up = dragend.bind(o); - d.addEventListener('mousemove', o.move, false); - return d.addEventListener('mouseup', o.up, false); + $.on(d, 'mousemove', o.move); + return $.on(d, 'mouseup', o.up); } }; touchmove = function(e) { @@ -2750,17 +2737,16 @@ }; dragend = function() { if (this.isTouching) { - d.removeEventListener('touchmove', this.move, false); - d.removeEventListener('touchend', this.up, false); - d.removeEventListener('touchcancel', this.up, false); + $.off(d, 'touchmove', this.move); + $.off(d, 'touchend touchcancel', this.up); } else { - d.removeEventListener('mousemove', this.move, false); - d.removeEventListener('mouseup', this.up, false); + $.off(d, 'mousemove', this.move); + $.off(d, 'mouseup', this.up); } return localStorage.setItem("" + g.NAMESPACE + this.id + ".position", this.style.cssText); }; hoverstart = function(_arg) { - var asap, asapTest, cb, el, endEvents, event, latestEvent, o, root, _i, _len, _ref; + var asapTest, cb, el, endEvents, latestEvent, o, root; root = _arg.root, el = _arg.el, latestEvent = _arg.latestEvent, endEvents = _arg.endEvents, asapTest = _arg.asapTest, cb = _arg.cb; o = { @@ -2768,59 +2754,41 @@ el: el, style: el.style, cb: cb, - endEvents: endEvents.split(' '), + endEvents: endEvents, latestEvent: latestEvent, clientHeight: doc.clientHeight, clientWidth: doc.clientWidth }; o.hover = hover.bind(o); o.hoverend = hoverend.bind(o); - asap = function() { - if (asapTest()) { + $.asap(function() { + return !el.parentNode || asapTest(); + }, function() { + if (el.parentNode) { return o.hover(o.latestEvent); - } else { - return o.timeout = setTimeout(asap, 25); } - }; - asap(); - _ref = o.endEvents; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - event = _ref[_i]; - root.addEventListener(event, o.hoverend, false); - } - return root.addEventListener('mousemove', o.hover, false); + }); + $.on(root, endEvents, o.hoverend); + return $.on(root, 'mousemove', o.hover); }; hover = function(e) { - var clientX, clientY, height, left, right, style, top; + var clientX, clientY, height, left, right, style, top, _ref; this.latestEvent = e; height = this.el.offsetHeight; clientX = e.clientX, clientY = e.clientY; top = clientY - 120; top = this.clientHeight <= height || top <= 0 ? 0 : top + height >= this.clientHeight ? this.clientHeight - height : top; - if (clientX <= this.clientWidth - 400) { - left = clientX + 45 + 'px'; - right = null; - } else { - left = null; - right = this.clientWidth - clientX + 45 + 'px'; - } + _ref = clientX <= this.clientWidth - 400 ? [clientX + 45 + 'px', null] : [null, this.clientWidth - clientX + 45 + 'px'], left = _ref[0], right = _ref[1]; style = this.style; style.top = top + 'px'; style.left = left; return style.right = right; }; hoverend = function() { - var event, _i, _len, _ref; - - this.el.parentNode.removeChild(this.el); - _ref = this.endEvents; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - event = _ref[_i]; - this.root.removeEventListener(event, this.hoverend, false); - } - this.root.removeEventListener('mousemove', this.hover, false); - clearTimeout(this.timeout); + $.rm(this.el); + $.off(this.root, this.endEvents, this.hoverend); + $.off(this.root, 'mousemove', this.hover); if (this.cb) { return this.cb.call(this); } @@ -2928,14 +2896,6 @@ }; return $.on(d, 'DOMContentLoaded', cb); }, - sync: function(key, cb) { - key = "" + g.NAMESPACE + key; - return $.on(window, 'storage', function(e) { - if (e.key === key) { - return cb(JSON.parse(e.newValue)); - } - }); - }, formData: function(form) { var fd, key, val; @@ -2957,22 +2917,22 @@ return fd; }, ajax: function(url, callbacks, opts) { - var form, headers, key, r, type, upCallbacks, val; + var cred, form, headers, key, r, sync, type, upCallbacks, val; if (opts == null) { opts = {}; } - type = opts.type, headers = opts.headers, upCallbacks = opts.upCallbacks, form = opts.form; + type = opts.type, cred = opts.cred, headers = opts.headers, upCallbacks = opts.upCallbacks, form = opts.form, sync = opts.sync; r = new XMLHttpRequest(); type || (type = form && 'post' || 'get'); - r.open(type, url, true); + r.open(type, url, !sync); for (key in headers) { val = headers[key]; r.setRequestHeader(key, val); } $.extend(r, callbacks); $.extend(r.upload, upCallbacks); - r.withCredentials = type === 'post'; + r.withCredentials = cred; r.send(form); return r; }, @@ -2981,7 +2941,7 @@ reqs = {}; return function(url, cb) { - var req; + var req, rm; if (req = reqs[url]) { if (req.readyState === 4) { @@ -2991,23 +2951,22 @@ } return; } + rm = function() { + return delete reqs[url]; + }; req = $.ajax(url, { - onload: function() { + onload: function(e) { var _i, _len, _ref; _ref = this.callbacks; for (_i = 0, _len = _ref.length; _i < _len; _i++) { cb = _ref[_i]; - cb.call(this); + cb.call(this, e); } return delete this.callbacks; }, - onabort: function() { - return delete reqs[url]; - }, - onerror: function() { - return delete reqs[url]; - } + onabort: rm, + onerror: rm }); req.callbacks = [cb]; return reqs[url] = req; @@ -3220,8 +3179,28 @@ size = unit > 1 ? Math.round(size * 100) / 100 : Math.round(size); return "" + size + " " + ['B', 'KB', 'MB', 'GB'][unit]; }, - "delete": function(key) { - var keys, _i, _len; + syncing: {}, + sync: (function() { + window.addEventListener('storage', function(e) { + var cb; + + if (cb = $.syncing[e.key]) { + return cb(JSON.parse(e.newValue)); + } + }, false); + return function(key, cb) { + return $.syncing[g.NAMESPACE + key] = cb; + }; + })(), + item: function(key, val) { + var item; + + item = {}; + item[key] = val; + return item; + }, + "delete": function(keys) { + var key, _i, _len; if (!(keys instanceof Array)) { keys = [keys]; @@ -3233,21 +3212,48 @@ GM_deleteValue(key); } }, - get: function(key, defaultVal) { - var val; + get: function(key, val, cb) { + var items; - if (val = GM_getValue(g.NAMESPACE + key)) { - return JSON.parse(val); + if (typeof cb === 'function') { + items = $.item(key, val); } else { - return defaultVal; + items = key; + cb = val; } + return $.queueTask(function() { + for (key in items) { + if (val = GM_getValue(g.NAMESPACE + key)) { + items[key] = JSON.parse(val); + } + } + return cb(items); + }); }, - set: function(key, val) { - key = g.NAMESPACE + key; - val = JSON.stringify(val); - localStorage.setItem(key, val); - return GM_setValue(key, val); - } + set: (function() { + var set; + + set = function(key, val) { + key = g.NAMESPACE + key; + val = JSON.stringify(val); + if (key in $.syncing) { + localStorage.setItem(key, val); + } + return GM_setValue(key, val); + }; + return function(keys, val) { + var key; + + if (typeof keys === 'string') { + set(keys, val); + return; + } + for (key in keys) { + val = keys[key]; + set(key, val); + } + }; + })() }); Polyfill = { @@ -4898,10 +4904,10 @@ if (/^[^\w@]/.test(t)) { return $.tn(t); } - if (t === 'toggle-all') { + if (/^toggle-all/.test(t)) { a = $.el('a', { className: 'show-board-list-button', - textContent: '+', + textContent: (t.match(/-text:"(.+)"/) || [null, '+'])[1], href: 'javascript:;' }); $.on(a, 'click', Header.toggleBoardList); @@ -5074,7 +5080,7 @@ Settings = { init: function() { - var link, prevVersion, settings; + var link, settings; link = $.el('a', { id: 'appchanOptions', @@ -5094,13 +5100,37 @@ return $.prepend($.id('navtopright'), [$.tn(' ['), link, $.tn('] ')]); }); }); - if ((prevVersion = $.get('previousversion', null)) !== g.VERSION) { - $.set('lastupdate', Date.now()); - $.set('previousversion', g.VERSION); - if (!prevVersion) { + $.get('previousversion', null, function(item) { + var changelog, curr, el, prev, previous; + + el = $.el('span'); + el.style.flex = 'test'; + if (el.style.flex === 'test') { + el.innerHTML = "Firefox is not correctly set up and some appchan x features will be displayed incorrectly.
\nFollow the instructions of the install guide to fix it."; + new Notification('warning', el, 30); + } + if (previous = item['previousversion']) { + if (previous === g.VERSION) { + return; + } + prev = previous.match(/\d+/g).map(Number); + curr = g.VERSION.match(/\d+/g).map(Number); + if (!(prev[0] <= curr[0] && prev[1] <= curr[1] && prev[2] <= curr[2])) { + return; + } + changelog = 'https://github.com/zixaphir/appchan-x/blob/Av2/CHANGELOG.md'; + el = $.el('span', { + innerHTML: "appchan x has been updated to version " + g.VERSION + "." + }); + new Notification('info', el, 30); + } else { $.on(d, '4chanXInitFinished', Settings.open); } - } + return $.set({ + lastupdate: Date.now(), + previousversion: g.VERSION + }); + }); Settings.addSection('Main', Settings.main); Settings.addSection('Filter', Settings.filter); Settings.addSection('Sauce', Settings.sauce); @@ -5127,7 +5157,7 @@ return; } $.event('CloseMenu'); - html = "
\n \n
\n
\n
"; + html = "
\n \n
\n
\n
"; Settings.dialog = overlay = $.el('div', { id: 'overlay', innerHTML: html @@ -5196,12 +5226,14 @@ return section.scrollTop = 0; }, main: function(section) { - var ID, arr, checked, description, div, fs, hiddenNum, key, obj, post, thread, _ref, _ref1, _ref2; + var arr, button, description, div, fs, hiddenNum, input, inputs, items, key, obj, _ref; section.innerHTML = "
\n \n \n \n
\n

"; $.on($('.export', section), 'click', Settings["export"]); $.on($('.import', section), 'click', Settings["import"]); $.on($('input', section), 'change', Settings.onImport); + items = {}; + inputs = {}; _ref = Config.main; for (key in _ref) { obj = _ref[key]; @@ -5210,49 +5242,101 @@ }); for (key in obj) { arr = obj[key]; - checked = $.get(key, Conf[key]) ? 'checked' : ''; description = arr[1]; div = $.el('div', { - innerHTML: ": " + description + "" + innerHTML: ": " + description + "" }); - $.on($('input', div), 'change', $.cb.checked); + input = $('input', div); + $.on(input, 'change', $.cb.checked); + items[key] = Conf[key]; + inputs[key] = input; $.add(fs, div); } $.add(section, fs); } - hiddenNum = 0; - _ref1 = ThreadHiding.getHiddenThreads().threads; - for (ID in _ref1) { - thread = _ref1[ID]; - hiddenNum++; - } - _ref2 = ReplyHiding.getHiddenPosts().threads; - for (ID in _ref2) { - thread = _ref2[ID]; - for (ID in thread) { - post = thread[ID]; - hiddenNum++; + $.get(items, function(items) { + var val; + + for (key in items) { + val = items[key]; + inputs[key].checked = val; } - } - div = $.el('div', { - innerHTML: ": Clear manually hidden threads and posts on /" + g.BOARD + "/." }); - $.on($('button', div), 'click', function() { + div = $.el('div', { + innerHTML: ": Clear manually-hidden threads and posts on all boards. Refresh the page to apply." + }); + button = $('button', div); + hiddenNum = 0; + $.get('hiddenThreads', { + boards: {} + }, function(item) { + var ID, board, thread, _ref1; + + _ref1 = item.hiddenThreads.boards; + for (ID in _ref1) { + board = _ref1[ID]; + for (ID in board) { + thread = board[ID]; + hiddenNum++; + } + } + return button.textContent = "Hidden: " + hiddenNum; + }); + $.get('hiddenPosts', { + boards: {} + }, function(item) { + var ID, board, post, thread, _ref1; + + _ref1 = item.hiddenPosts.boards; + for (ID in _ref1) { + board = _ref1[ID]; + for (ID in board) { + thread = board[ID]; + for (ID in thread) { + post = thread[ID]; + hiddenNum++; + } + } + } + return button.textContent = "Hidden: " + hiddenNum; + }); + $.on(button, 'click', function() { this.textContent = 'Hidden: 0'; - return $["delete"](["hiddenThreads." + g.BOARD, "hiddenPosts." + g.BOARD]); + return $.get('hiddenThreads', { + boards: {} + }, function(item) { + var boardID; + + for (boardID in item.hiddenThreads.boards) { + localStorage.removeItem("4chan-hide-t-" + boardID); + } + return $["delete"](['hiddenThreads', 'hiddenPosts']); + }); }); return $.after($('input[name="Stubs"]', section).parentNode.parentNode, div); }, - "export": function() { - var a, data, now, output; + "export": function(now, data) { + var a, db, p, _i, _len; - now = Date.now(); - data = { - version: g.VERSION, - date: now, - Conf: Conf, - WatchedThreads: $.get('WatchedThreads', {}) - }; + if (typeof now !== 'number') { + now = Date.now(); + data = { + version: g.VERSION, + date: now + }; + Conf['WatchedThreads'] = {}; + for (_i = 0, _len = DataBoards.length; _i < _len; _i++) { + db = DataBoards[_i]; + Conf[db] = { + boards: {} + }; + } + $.get(Conf, function(Conf) { + data.Conf = Conf; + return Settings["export"](now, data); + }); + return; + } a = $.el('a', { className: 'warning', textContent: 'Save me!', @@ -5264,9 +5348,9 @@ a.click(); return; } - output = this.parentNode.nextElementSibling; - output.innerHTML = null; - return $.add(output, a); + p = $('.imp-exp-result', Settings.dialog); + p.innerHTML = null; + return $.add(p, a); }, "import": function() { return this.nextElementSibling.click(); @@ -5287,7 +5371,7 @@ var data, err; try { - data = JSON.parse(decodeURIComponent(escape(e.target.result))); + data = JSON.parse(e.target.result); Settings.loadSettings(data); if (confirm('Import successful. Refresh now?')) { return window.location.reload(); @@ -5295,13 +5379,13 @@ } catch (_error) { err = _error; output.textContent = 'Import failed due to an error.'; - return c.log(err.stack); + return c.error(err.stack); } }; return reader.readAsText(file); }, loadSettings: function(data) { - var key, val, version, _ref, _ref1; + var key, val, version, _ref; version = data.version.split('.'); if (version[0] === '2') { @@ -5377,13 +5461,9 @@ return "Shift+" + s.slice(0, -1) + (s.slice(-1).toLowerCase()); }); } + data.Conf.WatchedThreads = data.WatchedThreads; } - _ref1 = data.Conf; - for (key in _ref1) { - val = _ref1[key]; - $.set(key, val); - } - return $.set('WatchedThreads', data.WatchedThreads); + return $.set(data.Conf); }, convertSettings: function(data, map) { var newKey, prevKey; @@ -5414,9 +5494,11 @@ ta = $.el('textarea', { name: name, className: 'field', - value: $.get(name, Conf[name]), spellcheck: false }); + $.get(name, Conf[name], function(item) { + return ta.value = item[name]; + }); $.on(ta, 'change', $.cb.value); $.add(div, ta); return; @@ -5428,25 +5510,39 @@ section.innerHTML = "
Sauce is disabled.
\n
Lines starting with a # will be ignored.
\n
You can specify a display text by appending ;text:[text] to the URL.
\n
    These parameters will be replaced by their corresponding values:\n
  • %TURL: Thumbnail URL.
  • \n
  • %URL: Full image URL.
  • \n
  • %MD5: MD5 hash.
  • \n
  • %board: Current board.
  • \n
\n"; sauce = $('textarea', section); - sauce.value = $.get('sauces', Conf['sauces']); + $.get('sauces', Conf['sauces'], function(item) { + return sauce.value = item['sauces']; + }); return $.on(sauce, 'change', $.cb.value); }, rice: function(section) { - var event, input, name, _i, _len, _ref; + var event, input, inputs, items, name, _i, _len, _ref; section.innerHTML = "
\n Custom Board Navigation is disabled.\n
\n
In the following, board can translate to a board ID (a, b, etc...), the current board (current), or the Status/Twitter link (status, @).
\n
Board link: board
\n
Title link: board-title
\n
Full text link: board-full
\n
Custom text link: board-text:\"VIP Board\"
\n
Index-only link: board-index
\n
Catalog-only link: board-catalog
\n
Combinations are possible: board-index-text:\"VIP Index\"
\n
Full board list toggle: toggle-all
\n
\n\n
\n Time Formatting is disabled.\n
:
\n
Supported format specifiers:
\n
Day: %a, %A, %d, %e
\n
Month: %m, %b, %B
\n
Year: %y
\n
Hour: %k, %H, %l, %I, %p, %P
\n
Minute: %M
\n
Second: %S
\n
\n\n
\n Quote Backlinks formatting is disabled.\n
:
\n
\n\n
\n File Info Formatting is disabled.\n
:
\n
Link: %l (truncated), %L (untruncated), %T (Unix timestamp)
\n
Original file name: %n (truncated), %N (untruncated), %t (Unix timestamp)
\n
Spoiler indicator: %p
\n
Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
\n
Resolution: %r (Displays 'PDF' for PDF files)
\n
\n\n
\n Unread Tab Icon is disabled.\n \n \n
\n\n
\n Custom CSS\n \n \n
"; + items = {}; + inputs = {}; _ref = ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { name = _ref[_i]; input = $("[name=" + name + "]", section); - input.value = $.get(name, Conf[name]); + items[name] = Conf[name]; + inputs[name] = input; event = ['favicon', 'usercss'].contains(name) ? 'change' : 'input'; $.on(input, event, $.cb.value); - if ('usercss' !== name) { - $.on(input, event, Settings[name]); - Settings[name].call(input); - } } + $.get(items, function(items) { + var key, val; + + for (key in items) { + val = items[key]; + input = inputs[key]; + input.value = val; + if ('usercss' !== name) { + $.on(input, event, Settings[key]); + Settings[key].call(input); + } + } + }); $.on($('input[name="Custom CSS"]', section), 'change', Settings.togglecss); return $.on($.id('apply-css'), 'click', Settings.usercss); }, @@ -5499,10 +5595,12 @@ return CustomCSS.update(); }, keybinds: function(section) { - var arr, input, key, tbody, tr, _ref; + var arr, input, inputs, items, key, tbody, tr, _ref; section.innerHTML = "
Keybinds are disabled.
\n
Allowed keys: a-z, 0-9, Ctrl, Shift, Alt, Meta, Enter, Esc, Up, Down, Right, Left.
\n
Press Backspace to disable a keybind.
\n\n \n
ActionsKeybinds
"; tbody = $('tbody', section); + items = {}; + inputs = {}; _ref = Config.hotkeys; for (key in _ref) { arr = _ref[key]; @@ -5511,11 +5609,20 @@ }); input = $('input', tr); input.name = key; - input.value = $.get(key, Conf[key]); input.spellcheck = false; + items[key] = Conf[key]; + inputs[key] = input; $.on(input, 'keydown', Settings.keybind); $.add(tbody, tr); } + return $.get(items, function(items) { + var val; + + for (key in items) { + val = items[key]; + inputs[key].value = val; + } + }); }, keybind: function(e) { var key; @@ -5717,7 +5824,7 @@ } if (result.hide) { if (this.isReply) { - ReplyHiding.hide(this, result.stub); + PostHiding.hide(this, result.stub); } else if (g.VIEW === 'index') { ThreadHiding.hide(this.thread, result.stub); } else { @@ -5856,7 +5963,7 @@ }; }, makeFilter: function() { - var re, save, section, select, ta, tl, type, value; + var re, section, select, ta, tl, type, value; type = this.dataset.type; value = Filter[type](Filter.menu.post); @@ -5873,9 +5980,13 @@ if (!Filter.menu.post.isReply) { re += ';op:yes'; } - save = $.get(type, ''); - save = save ? "" + save + "\n" + re : re; - $.set(type, save); + $.get(type, '', function(item) { + var save; + + save = item[type]; + save = save ? "" + save + "\n" + re : re; + return $.set(type, save); + }); Settings.open('Filter'); section = $('.section-container'); select = $('select[name=filter]', section); @@ -5894,9 +6005,8 @@ if (g.VIEW !== 'index' || !Conf['Thread Hiding'] && !Conf['Thread Hiding Link']) { return; } - Misc.clearThreads("hiddenThreads." + g.BOARD); - this.getHiddenThreads(); - this.syncFromCatalog(); + this.db = new DataBoard('hiddenThreads'); + this.syncCatalog(); return Thread.prototype.callbacks.push({ name: 'Thread Hiding', cb: this.node @@ -5905,7 +6015,10 @@ node: function() { var data; - if (data = ThreadHiding.hiddenThreads.threads[this]) { + if (data = ThreadHiding.db.get({ + boardID: this.board.ID, + threadID: this.ID + })) { ThreadHiding.hide(this, data.makeStub); } if (!Conf['Thread Hiding']) { @@ -5913,33 +6026,63 @@ } return $.prepend(this.OP.nodes.root, ThreadHiding.makeButton(this, 'hide')); }, - getHiddenThreads: function() { - return ThreadHiding.hiddenThreads = $.get("hiddenThreads." + g.BOARD, { - threads: {} + syncCatalog: function() { + var e, hiddenThreads, hiddenThreadsOnCatalog, threadID; + + hiddenThreads = ThreadHiding.db.get({ + boardID: g.BOARD.ID, + defaultValue: {} + }); + try { + hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem("4chan-hide-t-" + g.BOARD)) || {}; + } catch (_error) { + e = _error; + localStorage.setItem("4chan-hide-t-" + g.BOARD, JSON.stringify({})); + return ThreadHiding.syncCatalog(); + } + for (threadID in hiddenThreadsOnCatalog) { + if (!(threadID in hiddenThreads)) { + hiddenThreads[threadID] = {}; + } + } + for (threadID in hiddenThreads) { + if (!(threadID in hiddenThreadsOnCatalog)) { + delete hiddenThreads[threadID]; + } + } + if ((ThreadHiding.db.data.lastChecked || 0) > Date.now() - $.MINUTE) { + ThreadHiding.cleanCatalog(hiddenThreadsOnCatalog); + } + return ThreadHiding.db.set({ + boardID: g.BOARD.ID, + val: hiddenThreads }); }, - syncFromCatalog: function() { - var hiddenThreadsOnCatalog, threadID, threads; + cleanCatalog: function(hiddenThreadsOnCatalog) { + return $.cache("//api.4chan.org/" + g.BOARD + "/threads.json", function() { + var page, thread, threads, _i, _j, _len, _len1, _ref, _ref1; - hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem("4chan-hide-t-" + g.BOARD)) || {}; - threads = ThreadHiding.hiddenThreads.threads; - for (threadID in hiddenThreadsOnCatalog) { - if (threadID in threads) { - continue; + if (this.status !== 200) { + return; } - threads[threadID] = {}; - } - for (threadID in threads) { - if (threadID in threads) { - continue; + threads = {}; + _ref = JSON.parse(this.response); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + page = _ref[_i]; + _ref1 = page.threads; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + thread = _ref1[_j]; + if (thread.no in hiddenThreadsOnCatalog) { + threads[thread.no] = hiddenThreadsOnCatalog[thread.no]; + } + } } - delete threads[threadID]; - } - if (Object.keys(threads).length) { - return $.set("hiddenThreads." + g.BOARD, ThreadHiding.hiddenThreads); - } else { - return $["delete"]("hiddenThreads." + g.BOARD); - } + if (Object.keys(threads).length) { + return localStorage.setItem("4chan-hide-t-" + g.BOARD, JSON.stringify(threads)); + } else { + return localStorage.removeItem("4chan-hide-t-" + g.BOARD); + } + }); }, menu: { init: function() { @@ -6006,21 +6149,26 @@ return a; }, saveHiddenState: function(thread, makeStub) { - var hiddenThreads, hiddenThreadsCatalog; + var hiddenThreadsOnCatalog; - hiddenThreads = ThreadHiding.getHiddenThreads(); - hiddenThreadsCatalog = JSON.parse(localStorage.getItem("4chan-hide-t-" + g.BOARD)) || {}; + hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem("4chan-hide-t-" + g.BOARD)) || {}; if (thread.isHidden) { - hiddenThreads.threads[thread] = { - makeStub: makeStub - }; - hiddenThreadsCatalog[thread] = true; + ThreadHiding.db.set({ + boardID: thread.board.ID, + threadID: thread.ID, + val: { + makeStub: makeStub + } + }); + hiddenThreadsOnCatalog[thread] = true; } else { - delete hiddenThreads.threads[thread]; - delete hiddenThreadsCatalog[thread]; + ThreadHiding.db["delete"]({ + boardID: thread.board.ID, + threadID: thread.ID + }); + delete hiddenThreadsOnCatalog[thread]; } - $.set("hiddenThreads." + g.BOARD, hiddenThreads); - return localStorage.setItem("4chan-hide-t-" + g.BOARD, JSON.stringify(hiddenThreadsCatalog)); + return localStorage.setItem("4chan-hide-t-" + g.BOARD, JSON.stringify(hiddenThreadsOnCatalog)); }, toggle: function(thread) { if (!(thread instanceof Thread)) { @@ -6079,32 +6227,33 @@ } }; - ReplyHiding = { + PostHiding = { init: function() { if (g.VIEW === 'catalog' || !Conf['Reply Hiding'] && !Conf['Reply Hiding Link']) { return; } - Misc.clearThreads("hiddenPosts." + g.BOARD); - this.getHiddenPosts(); + this.db = new DataBoard('hiddenPosts'); return Post.prototype.callbacks.push({ name: 'Reply Hiding', cb: this.node }); }, node: function() { - var data, thread; + var data; if (!this.isReply || this.isClone) { return; } - if (thread = ReplyHiding.hiddenPosts.threads[this.thread]) { - if (data = thread[this]) { - if (data.thisPost) { - ReplyHiding.hide(this, data.makeStub, data.hideRecursively); - } else { - Recursive.apply(ReplyHiding.hide, this, data.makeStub, true); - Recursive.add(ReplyHiding.hide, this, data.makeStub, true); - } + if (data = PostHiding.db.get({ + boardID: this.board.ID, + threadID: this.thread.ID, + postID: this.ID + })) { + if (data.thisPost) { + PostHiding.hide(this, data.makeStub, data.hideRecursively); + } else { + Recursive.apply(PostHiding.hide, this, data.makeStub, true); + Recursive.add(PostHiding.hide, this, data.makeStub, true); } } if (!Conf['Reply Hiding']) { @@ -6112,11 +6261,6 @@ } return $.add($('.postInfo', this.nodes.post), ReplyHiding.makeButton(this, 'hide')); }, - getHiddenPosts: function() { - return ReplyHiding.hiddenPosts = $.get("hiddenPosts." + g.BOARD, { - threads: {} - }); - }, menu: { init: function() { var apply, div, makeStub, replies, thisPost; @@ -6132,7 +6276,7 @@ textContent: 'Apply', href: 'javascript:;' }); - $.on(apply, 'click', ReplyHiding.menu.hide); + $.on(apply, 'click', PostHiding.menu.hide); thisPost = $.el('label', { innerHTML: ' This post' }); @@ -6150,7 +6294,7 @@ if (!post.isReply || post.isClone || post.isHidden) { return false; } - ReplyHiding.menu.post = post; + PostHiding.menu.post = post; return true; }, subEntries: [ @@ -6173,7 +6317,7 @@ textContent: 'Apply', href: 'javascript:;' }); - $.on(apply, 'click', ReplyHiding.menu.show); + $.on(apply, 'click', PostHiding.menu.show); thisPost = $.el('label', { innerHTML: ' This post' }); @@ -6185,16 +6329,19 @@ el: div, order: 20, open: function(post) { - var data, thread; + var data; - if (!post.isReply || post.isClone) { + if (!post.isReply || post.isClone || !post.isHidden) { return false; } - thread = ReplyHiding.getHiddenPosts().threads[post.thread]; - if (!(post.isHidden || (data = thread != null ? thread[post] : void 0))) { + if (!(data = PostHiding.db.get({ + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + }))) { return false; } - ReplyHiding.menu.post = post; + PostHiding.menu.post = post; thisPost.firstChild.checked = post.isHidden; replies.firstChild.checked = (data != null ? data.hideRecursively : void 0) != null ? data.hideRecursively : Conf['Recursive Hiding']; return true; @@ -6217,37 +6364,39 @@ thisPost = $('input[name=thisPost]', parent).checked; replies = $('input[name=replies]', parent).checked; makeStub = $('input[name=makeStub]', parent).checked; - post = ReplyHiding.menu.post; + post = PostHiding.menu.post; if (thisPost) { - ReplyHiding.hide(post, makeStub, replies); + PostHiding.hide(post, makeStub, replies); } else if (replies) { - Recursive.apply(ReplyHiding.hide, post, makeStub, true); - Recursive.add(ReplyHiding.hide, post, makeStub, true); + Recursive.apply(PostHiding.hide, post, makeStub, true); + Recursive.add(PostHiding.hide, post, makeStub, true); } else { return; } - ReplyHiding.saveHiddenState(post, true, thisPost, makeStub, replies); + PostHiding.saveHiddenState(post, true, thisPost, makeStub, replies); return $.event('CloseMenu'); }, show: function() { - var data, parent, post, replies, thisPost, thread; + var data, parent, post, replies, thisPost; parent = this.parentNode; thisPost = $('input[name=thisPost]', parent).checked; replies = $('input[name=replies]', parent).checked; - post = ReplyHiding.menu.post; - thread = ReplyHiding.getHiddenPosts().threads[post.thread]; - data = thread != null ? thread[post] : void 0; + post = PostHiding.menu.post; if (thisPost) { - ReplyHiding.show(post, replies); + PostHiding.show(post, replies); } else if (replies) { - Recursive.apply(ReplyHiding.show, post, true); - Recursive.rm(ReplyHiding.hide, post, true); + Recursive.apply(PostHiding.show, post, true); + Recursive.rm(PostHiding.hide, post, true); } else { return; } - if (data) { - ReplyHiding.saveHiddenState(post, !(thisPost && replies), !thisPost, data.makeStub, !replies); + if (data = PostHiding.db.get({ + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + })) { + PostHiding.saveHiddenState(post, !(thisPost && replies), !thisPost, data.makeStub, !replies); } return $.event('CloseMenu'); } @@ -6260,41 +6409,38 @@ innerHTML: "[ " + (type === 'hide' ? '-' : '+') + " ]", href: 'javascript:;' }); - $.on(a, 'click', ReplyHiding.toggle); + $.on(a, 'click', PostHiding.toggle); return a; }, saveHiddenState: function(post, isHiding, thisPost, makeStub, hideRecursively) { - var hiddenPosts, thread; + var data; - hiddenPosts = ReplyHiding.getHiddenPosts(); + data = { + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + }; if (isHiding) { - if (!(thread = hiddenPosts.threads[post.thread])) { - thread = hiddenPosts.threads[post.thread] = {}; - } - thread[post] = { + data.val = { thisPost: thisPost !== false, makeStub: makeStub, hideRecursively: hideRecursively }; + return PostHiding.db.set(data); } else { - thread = hiddenPosts.threads[post.thread]; - delete thread[post]; - if (!Object.keys(thread).length) { - delete hiddenPosts.threads[post.thread]; - } + return PostHiding.db["delete"](data); } - return $.set("hiddenPosts." + g.BOARD, hiddenPosts); }, toggle: function() { var post; post = Get.postFromNode(this); if (post.isHidden) { - ReplyHiding.show(post); + PostHiding.show(post); } else { - ReplyHiding.hide(post); + PostHiding.hide(post); } - return ReplyHiding.saveHiddenState(post, post.isHidden); + return PostHiding.saveHiddenState(post, post.isHidden); }, hide: function(post, makeStub, hideRecursively) { var a, postInfo, quotelink, _i, _len, _ref; @@ -6310,8 +6456,8 @@ } post.isHidden = true; if (hideRecursively) { - Recursive.apply(ReplyHiding.hide, post, makeStub, true); - Recursive.add(ReplyHiding.hide, post, makeStub, true); + Recursive.apply(PostHiding.hide, post, makeStub, true); + Recursive.add(PostHiding.hide, post, makeStub, true); } _ref = Get.allQuotelinksLinkingTo(post); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -6322,7 +6468,7 @@ post.nodes.root.hidden = true; return; } - a = ReplyHiding.makeButton(post, 'show'); + a = PostHiding.makeButton(post, 'show'); postInfo = Conf['Anonymize'] ? 'Anonymous' : $('.nameBlock', post.nodes.info).textContent; $.add(a, $.tn(" " + postInfo)); post.nodes.stub = $.el('div', { @@ -6348,8 +6494,8 @@ } post.isHidden = false; if (showRecursively) { - Recursive.apply(ReplyHiding.show, post, true); - Recursive.rm(ReplyHiding.hide, post); + Recursive.apply(PostHiding.show, post, true); + Recursive.rm(PostHiding.hide, post); } _ref = Get.allQuotelinksLinkingTo(post); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -6440,7 +6586,7 @@ }); }, node: function() { - var board, postID, quotelink, _i, _len, _ref, _ref1, _ref2; + var boardID, postID, quotelink, _i, _len, _ref, _ref1, _ref2; if (this.isClone) { return; @@ -6448,8 +6594,8 @@ _ref = this.nodes.quotelinks; for (_i = 0, _len = _ref.length; _i < _len; _i++) { quotelink = _ref[_i]; - _ref1 = Get.postDataFromLink(quotelink), board = _ref1.board, postID = _ref1.postID; - if ((_ref2 = g.posts["" + board + "." + postID]) != null ? _ref2.isHidden : void 0) { + _ref1 = Get.postDataFromLink(quotelink), boardID = _ref1.boardID, postID = _ref1.postID; + if ((_ref2 = g.posts["" + boardID + "." + postID]) != null ? _ref2.isHidden : void 0) { $.addClass(quotelink, 'filtered'); } } @@ -6586,21 +6732,15 @@ el: div, order: 40, open: function(post) { - var node, seconds, thread; + var node; - if (post.isDead || !((thread = QR.yourPosts.threads[post.thread]) && thread.contains(post.ID))) { + if (post.isDead) { return false; } DeleteLink.post = post; - DeleteLink.cooldown.start(post); node = div.firstChild; - if (seconds = DeleteLink.cooldown[post.fullID]) { - node.textContent = "Delete (" + seconds + ")"; - DeleteLink.cooldown.el = node; - } else { - node.textContent = 'Delete'; - delete DeleteLink.cooldown.el; - } + node.textContent = 'Delete'; + DeleteLink.cooldown.start(post, node); return true; }, subEntries: [postEntry, fileEntry] @@ -6610,7 +6750,7 @@ var form, link, m, post, pwd; post = DeleteLink.post; - if (DeleteLink.cooldown[post.fullID]) { + if (DeleteLink.cooldown.counting === post) { return; } $.off(this, 'click', DeleteLink["delete"]); @@ -6625,16 +6765,17 @@ link = this; return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), { onload: function() { - return DeleteLink.load(link, this.response); + return DeleteLink.load(link, post, this.response); }, onerror: function() { return DeleteLink.error(link); } }, { + cred: true, form: $.formData(form) }); }, - load: function(link, html) { + load: function(link, post, html) { var msg, s, tmpDoc; tmpDoc = d.implementation.createHTMLDocument(''); @@ -6645,6 +6786,9 @@ s = msg.textContent; $.on(link, 'click', DeleteLink["delete"]); } else { + if (tmpDoc.title === 'Updating index...') { + (post.origin || post).kill(); + } s = 'Deleted'; } return link.textContent = s; @@ -6654,36 +6798,38 @@ return $.on(link, 'click', DeleteLink["delete"]); }, cooldown: { - start: function(post) { - var length, seconds; + start: function(post, node) { + var length, seconds, _ref; - if (post.fullID in DeleteLink.cooldown) { + if (!((_ref = QR.db) != null ? _ref.get({ + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + }) : void 0)) { + delete DeleteLink.cooldown.counting; return; } + DeleteLink.cooldown.counting = post; length = post.board.ID === 'q' ? 600 : 30; seconds = Math.ceil((length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND); - return DeleteLink.cooldown.count(post.fullID, seconds, length); + return DeleteLink.cooldown.count(post, seconds, length, node); }, - count: function(fullID, seconds, length) { - var el; - + count: function(post, seconds, length, node) { + if (DeleteLink.cooldown.counting !== post) { + return; + } if (!((0 <= seconds && seconds <= length))) { - return; - } - setTimeout(DeleteLink.cooldown.count, 1000, fullID, seconds - 1, length); - el = DeleteLink.cooldown.el; - if (seconds === 0) { - if (el != null) { - el.textContent = 'Delete'; + if (DeleteLink.cooldown.counting === post) { + delete DeleteLink.cooldown.counting; } - delete DeleteLink.cooldown[fullID]; - delete DeleteLink.cooldown.el; return; } - if (el != null) { - el.textContent = "Delete (" + seconds + ")"; + setTimeout(DeleteLink.cooldown.count, 1000, post, seconds - 1, length, node); + if (seconds === 0) { + node.textContent = 'Delete'; + return; } - return DeleteLink.cooldown[fullID] = seconds; + return node.textContent = "Delete (" + seconds + ")"; } } }; @@ -6736,13 +6882,13 @@ el: div, order: 90, open: function(_arg) { - var board, postID, redirect, threadID; + var ID, board, redirect, thread; - postID = _arg.ID, threadID = _arg.thread, board = _arg.board; + ID = _arg.ID, thread = _arg.thread, board = _arg.board; redirect = Redirect.to({ - postID: postID, - threadID: threadID, - board: board + postID: ID, + threadID: thread.ID, + boardID: board.ID }); return redirect !== ("//boards.4chan.org/" + board + "/"); }, @@ -6762,35 +6908,31 @@ textContent: text, target: '_blank' }); - if (type === 'post') { - open = function(_arg) { - var board, postID, threadID; + open = type === 'post' ? function(_arg) { + var ID, board, thread; - postID = _arg.ID, threadID = _arg.thread, board = _arg.board; - el.href = Redirect.to({ - postID: postID, - threadID: threadID, - board: board - }); - return true; - }; - } else { - open = function(post) { - var value; + ID = _arg.ID, thread = _arg.thread, board = _arg.board; + el.href = Redirect.to({ + postID: ID, + threadID: thread.ID, + boardID: board.ID + }); + return true; + } : function(post) { + var value; - value = Filter[type](post); - if (!value) { - return false; - } - el.href = Redirect.to({ - board: post.board, - type: type, - value: value, - isSearch: true - }); - return true; - }; - } + value = Filter[type](post); + if (!value) { + return false; + } + el.href = Redirect.to({ + boardID: post.board.ID, + type: type, + value: value, + isSearch: true + }); + return true; + }; return { el: el, open: open @@ -7089,7 +7231,7 @@ init: function() { var next, prev, span; - if (g.VIEW !== 'index' || !Conf['Index Navigation']) { + if (g.VIEW === 'index' && !Conf['Index Navigation'] || g.VIEW === 'thread' && !Conf['Reply Navigation']) { return; } span = $.el('span', { @@ -7111,10 +7253,18 @@ }); }, prev: function() { - return Nav.scroll(-1); + if (g.VIEW === 'thread') { + return window.scrollTo(0, 0); + } else { + return Nav.scroll(-1); + } }, next: function() { - return Nav.scroll(+1); + if (g.VIEW === 'thread') { + return window.scrollTo(0, d.body.scrollHeight); + } else { + return Nav.scroll(+1); + } }, getThread: function(full) { var headRect, i, rect, thread, threads, topMargin, _i, _len; @@ -7149,41 +7299,47 @@ }; Redirect = { - image: function(board, filename) { - switch ("" + board) { + image: function(boardID, filename) { + switch (boardID) { case 'a': + case 'gd': case 'jp': case 'm': case 'q': case 'tg': case 'vg': + case 'vp': + case 'vr': case 'wsg': - return "//archive.foolz.us/" + board + "/full_image/" + filename; + return "//archive.foolz.us/" + boardID + "/full_image/" + filename; case 'u': - return "//nsfw.foolz.us/" + board + "/full_image/" + filename; + return "//nsfw.foolz.us/" + boardID + "/full_image/" + filename; case 'po': - return "//archive.thedarkcave.org/" + board + "/full_image/" + filename; + return "//archive.thedarkcave.org/" + boardID + "/full_image/" + filename; case 'ck': + case 'fa': case 'lit': - return "//fuuka.warosu.org/" + board + "/full_image/" + filename; + case 's4s': + return "//fuuka.warosu.org/" + boardID + "/full_image/" + filename; case 'cgl': case 'g': case 'mu': case 'w': - return "//rbt.asia/" + board + "/full_image/" + filename; + return "//rbt.asia/" + boardID + "/full_image/" + filename; case 'an': case 'k': case 'toy': case 'x': - return "http://archive.heinessen.com/" + board + "/full_image/" + filename; + return "http://archive.heinessen.com/" + boardID + "/full_image/" + filename; case 'c': - return "//archive.nyafuu.org/" + board + "/full_image/" + filename; + return "//archive.nyafuu.org/" + boardID + "/full_image/" + filename; } }, - post: function(board, postID) { - switch ("" + board) { + post: function(boardID, postID) { + switch (boardID) { case 'a': case 'co': + case 'gd': case 'jp': case 'm': case 'q': @@ -7192,23 +7348,27 @@ case 'tv': case 'v': case 'vg': + case 'vp': + case 'vr': case 'wsg': - return "//archive.foolz.us/_/api/chan/post/?board=" + board + "&num=" + postID; + return "//archive.foolz.us/_/api/chan/post/?board=" + boardID + "&num=" + postID; case 'u': - return "//nsfw.foolz.us/_/api/chan/post/?board=" + board + "&num=" + postID; + return "//nsfw.foolz.us/_/api/chan/post/?board=" + boardID + "&num=" + postID; case 'c': case 'int': + case 'out': case 'po': - return "//archive.thedarkcave.org/_/api/chan/post/?board=" + board + "&num=" + postID; + return "//archive.thedarkcave.org/_/api/chan/post/?board=" + boardID + "&num=" + postID; } }, to: function(data) { - var board, url; + var boardID; - board = data.board; - switch ("" + board) { + boardID = data.boardID; + switch (boardID) { case 'a': case 'co': + case 'gd': case 'jp': case 'm': case 'q': @@ -7217,30 +7377,29 @@ case 'tv': case 'v': case 'vg': + case 'vp': + case 'vr': case 'wsg': - url = Redirect.path('//archive.foolz.us', 'foolfuuka', data); - break; + return Redirect.path('//archive.foolz.us', 'foolfuuka', data); case 'u': - url = Redirect.path('//nsfw.foolz.us', 'foolfuuka', data); - break; + return Redirect.path('//nsfw.foolz.us', 'foolfuuka', data); case 'int': + case 'out': case 'po': - url = Redirect.path('//archive.thedarkcave.org', 'foolfuuka', data); - break; + return Redirect.path('//archive.thedarkcave.org', 'foolfuuka', data); case 'ck': + case 'fa': case 'lit': - url = Redirect.path('//fuuka.warosu.org', 'fuuka', data); - break; + case 's4s': + return Redirect.path('//fuuka.warosu.org', 'fuuka', data); case 'diy': case 'sci': - url = Redirect.path('//archive.installgentoo.net', 'fuuka', data); - break; + return Redirect.path('//archive.installgentoo.net', 'fuuka', data); case 'cgl': case 'g': case 'mu': case 'w': - url = Redirect.path('//rbt.asia', 'fuuka', data); - break; + return Redirect.path('//rbt.asia', 'fuuka', data); case 'an': case 'fit': case 'k': @@ -7248,38 +7407,34 @@ case 'r9k': case 'toy': case 'x': - url = Redirect.path('http://archive.heinessen.com', 'fuuka', data); - break; + return Redirect.path('http://archive.heinessen.com', 'fuuka', data); case 'c': - url = Redirect.path('//archive.nyafuu.org', 'fuuka', data); - break; + return Redirect.path('//archive.nyafuu.org', 'fuuka', data); default: if (data.threadID) { - url = "//boards.4chan.org/" + board + "/"; + return "//boards.4chan.org/" + boardID + "/"; + } else { + return ''; } } - return url || ''; }, path: function(base, archiver, data) { - var board, path, postID, threadID, type, value; + var boardID, path, postID, threadID, type, value; if (data.isSearch) { - board = data.board, type = data.type, value = data.value; + boardID = data.boardID, type = data.type, value = data.value; type = type === 'name' ? 'username' : type === 'MD5' ? 'image' : type; value = encodeURIComponent(value); if (archiver === 'foolfuuka') { - return "" + base + "/" + board + "/search/" + type + "/" + value; + return "" + base + "/" + boardID + "/search/" + type + "/" + value; } else if (type === 'image') { - return "" + base + "/" + board + "/?task=search2&search_media_hash=" + value; + return "" + base + "/" + boardID + "/?task=search2&search_media_hash=" + value; } else { - return "" + base + "/" + board + "/?task=search2&search_" + type + "=" + value; + return "" + base + "/" + boardID + "/?task=search2&search_" + type + "=" + value; } } - board = data.board, threadID = data.threadID, postID = data.postID; - if (postID && typeof postID === 'string') { - postID = postID.match(/\d+/)[0]; - } - path = threadID ? "" + board + "/thread/" + threadID : "" + board + "/post/" + postID; + boardID = data.boardID, threadID = data.threadID, postID = data.postID; + path = threadID ? "" + boardID + "/thread/" + threadID : "" + boardID + "/post/" + postID; if (archiver === 'foolfuuka') { path += '/'; } @@ -7302,13 +7457,13 @@ return filename; } }, - postFromObject: function(data, board) { + postFromObject: function(data, boardID) { var o; o = { postID: data.no, threadID: data.resto || data.no, - board: board, + boardID: boardID, name: data.name, capcode: data.capcode, tripcode: data.trip, @@ -7327,12 +7482,12 @@ o.file = { name: data.filename + data.ext, timestamp: "" + data.tim + data.ext, - url: "//images.4chan.org/" + board + "/src/" + data.tim + data.ext, + url: "//images.4chan.org/" + boardID + "/src/" + data.tim + data.ext, height: data.h, width: data.w, MD5: data.md5, size: data.fsize, - turl: "//thumbs.4chan.org/" + board + "/thumb/" + data.tim + "s.jpg", + turl: "//thumbs.4chan.org/" + boardID + "/thumb/" + data.tim + "s.jpg", theight: data.tn_h, twidth: data.tn_w, isSpoiler: !!data.spoiler, @@ -7347,9 +7502,9 @@ @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE */ - var a, board, capcode, capcodeClass, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref; + var a, boardID, capcode, capcodeClass, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref; - postID = o.postID, threadID = o.threadID, board = o.board, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, file = o.file; + postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, file = o.file; isOP = postID === threadID; staticPath = '//static.4chan.org'; if (email) { @@ -7383,7 +7538,7 @@ capcodeStart = ''; capcode = ''; } - flag = flagCode ? ("  + flagCode + ") : ''; + flag = flagCode ? ("  + flagCode + ") : ''; if (file != null ? file.isDeleted : void 0) { fileHTML = isOP ? ("
") + ("File deleted.") + "
" : ("
") + ("File deleted.") + "
"; } else if (file) { @@ -7398,14 +7553,14 @@ fileSize = "Spoiler Image, " + fileSize; if (!isArchived) { fileThumb = '//static.4chan.org/image/spoiler'; - if (spoilerRange = Build.spoilerRange[board]) { - fileThumb += ("-" + board) + Math.floor(1 + spoilerRange * Math.random()); + if (spoilerRange = Build.spoilerRange[boardID]) { + fileThumb += ("-" + boardID) + Math.floor(1 + spoilerRange * Math.random()); } fileThumb += '.png'; file.twidth = file.theight = 100; } } - if (board.ID !== 'f') { + if (boardID.ID !== 'f') { imgSrc = ("") + ("" + fileSize + ""); } a = $.el('a', { @@ -7428,7 +7583,7 @@ container = $.el('div', { id: "pc" + postID, className: "postContainer " + (isOP ? 'op' : 'reply') + "Container", - innerHTML: (isOP ? '' : "
>>
") + ("
") + ("' + (isOP ? fileHTML : '') + ("' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
' + innerHTML: (isOP ? '' : "
>>
") + ("
") + ("' + (isOP ? fileHTML : '') + ("' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
' }); _ref = $$('.quotelink', container); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -7437,7 +7592,7 @@ if (href[0] === '/') { continue; } - quote.href = "/" + board + "/res/" + href; + quote.href = "/" + boardID + "/res/" + href; } return container; } @@ -7452,13 +7607,13 @@ return "/" + thread.board + "/ - " + excerpt; }, postFromRoot: function(root) { - var board, index, link, post, postID; + var boardID, index, link, post, postID; link = $('a[title="Highlight this post"]', root); - board = link.pathname.split('/')[1]; + boardID = link.pathname.split('/')[1]; postID = link.hash.slice(2); index = root.dataset.clone; - post = g.posts["" + board + "." + postID]; + post = g.posts["" + boardID + "." + postID]; if (index) { return post.clones[index]; } else { @@ -7472,20 +7627,20 @@ return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', quotelink)); }, postDataFromLink: function(link) { - var board, path, postID, threadID; + var boardID, path, postID, threadID; if (link.hostname === 'boards.4chan.org') { path = link.pathname.split('/'); - board = path[1]; + boardID = path[1]; threadID = path[3]; postID = link.hash.slice(2); } else { - board = link.dataset.board; + boardID = link.dataset.boardid; threadID = link.dataset.threadid || 0; postID = link.dataset.postid; } return { - board: board, + boardID: boardID, threadID: +threadID, postID: +postID }; @@ -7520,27 +7675,27 @@ } } return quotelinks.filter(function(quotelink) { - var board, postID, _ref4; + var boardID, postID, _ref4; - _ref4 = Get.postDataFromLink(quotelink), board = _ref4.board, postID = _ref4.postID; - return board === post.board.ID && postID === post.ID; + _ref4 = Get.postDataFromLink(quotelink), boardID = _ref4.boardID, postID = _ref4.postID; + return boardID === post.board.ID && postID === post.ID; }); }, - postClone: function(board, threadID, postID, root, context) { + postClone: function(boardID, threadID, postID, root, context) { var post, url; - if (post = g.posts["" + board + "." + postID]) { + if (post = g.posts["" + boardID + "." + postID]) { Get.insert(post, root, context); return; } root.textContent = "Loading post No." + postID + "..."; if (threadID) { - return $.cache("//api.4chan.org/" + board + "/res/" + threadID + ".json", function() { - return Get.fetchedPost(this, board, threadID, postID, root, context); + return $.cache("//api.4chan.org/" + boardID + "/res/" + threadID + ".json", function() { + return Get.fetchedPost(this, boardID, threadID, postID, root, context); }); - } else if (url = Redirect.post(board, postID)) { + } else if (url = Redirect.post(boardID, postID)) { return $.cache(url, function() { - return Get.archivedPost(this, board, postID, root, context); + return Get.archivedPost(this, boardID, postID, root, context); }); } }, @@ -7558,18 +7713,18 @@ root.innerHTML = null; return $.add(root, nodes.root); }, - fetchedPost: function(req, board, threadID, postID, root, context) { - var post, posts, status, thread, url, _i, _len; + fetchedPost: function(req, boardID, threadID, postID, root, context) { + var board, post, posts, status, thread, url, _i, _len; - if (post = g.posts["" + board + "." + postID]) { + if (post = g.posts["" + boardID + "." + postID]) { Get.insert(post, root, context); return; } status = req.status; if (![200, 304].contains(status)) { - if (url = Redirect.post(board, postID)) { + if (url = Redirect.post(boardID, postID)) { $.cache(url, function() { - return Get.archivedPost(this, board, postID, root, context); + return Get.archivedPost(this, boardID, postID, root, context); }); } else { $.addClass(root, 'warning'); @@ -7578,16 +7733,16 @@ return; } posts = JSON.parse(req.response).posts; - Build.spoilerRange[board] = posts[0].custom_spoiler; + Build.spoilerRange[boardID] = posts[0].custom_spoiler; for (_i = 0, _len = posts.length; _i < _len; _i++) { post = posts[_i]; if (post.no === postID) { break; } if (post.no > postID) { - if (url = Redirect.post(board, postID)) { + if (url = Redirect.post(boardID, postID)) { $.cache(url, function() { - return Get.archivedPost(this, board, postID, root, context); + return Get.archivedPost(this, boardID, postID, root, context); }); } else { $.addClass(root, 'warning'); @@ -7596,16 +7751,16 @@ return; } } - board = g.boards[board] || new Board(board); - thread = g.threads["" + board + "." + threadID] || new Thread(threadID, board); - post = new Post(Build.postFromObject(post, board), thread, board); + board = g.boards[boardID] || new Board(boardID); + thread = g.threads["" + boardID + "." + threadID] || new Thread(threadID, board); + post = new Post(Build.postFromObject(post, boardID), thread, board); Main.callbackNodes(Post, [post]); return Get.insert(post, root, context); }, - archivedPost: function(req, board, postID, root, context) { - var bq, comment, data, o, post, thread, threadID, _ref; + archivedPost: function(req, boardID, postID, root, context) { + var board, bq, comment, data, o, post, thread, threadID, _ref; - if (post = g.posts["" + board + "." + postID]) { + if (post = g.posts["" + boardID + "." + postID]) { Get.insert(post, root, context); return; } @@ -7649,7 +7804,7 @@ o = { postID: "" + postID, threadID: "" + threadID, - board: board, + boardID: boardID, name: data.name_processed, capcode: (function() { switch (data.capcode) { @@ -7680,14 +7835,14 @@ width: data.media.media_w, MD5: data.media.media_hash, size: data.media.media_size, - turl: data.media.thumb_link || ("//thumbs.4chan.org/" + board + "/thumb/" + data.media.preview_orig), + turl: data.media.thumb_link || ("//thumbs.4chan.org/" + boardID + "/thumb/" + data.media.preview_orig), theight: data.media.preview_h, twidth: data.media.preview_w, isSpoiler: data.media.spoiler === '1' }; } - board = g.boards[board] || new Board(board); - thread = g.threads["" + board + "." + threadID] || new Thread(threadID, board); + board = g.boards[boardID] || new Board(boardID); + thread = g.threads["" + boardID + "." + threadID] || new Thread(threadID, board); post = new Post(Build.post(o, true), thread, board, { isArchived: true }); @@ -7696,48 +7851,6 @@ } }; - Misc = { - clearThreads: function(key) { - var data; - - if (!(data = $.get(key))) { - return; - } - if (!Object.keys(data.threads).length) { - $["delete"](key); - return; - } - if (data.lastChecked > Date.now() - 12 * $.HOUR) { - return; - } - return $.ajax("//api.4chan.org/" + g.BOARD + "/threads.json", { - onload: function() { - var page, thread, threads, _i, _j, _len, _len1, _ref, _ref1; - - threads = {}; - _ref = JSON.parse(this.response); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - page = _ref[_i]; - _ref1 = page.threads; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - thread = _ref1[_j]; - if (thread.no in data.threads) { - threads[thread.no] = data.threads[thread.no]; - } - } - } - if (!Object.keys(threads).length) { - $["delete"](key); - return; - } - data.threads = threads; - data.lastChecked = Date.now(); - return $.set(key, data); - } - }); - } - }; - Quotify = { init: function() { if (g.VIEW === 'catalog' || !Conf['Resurrect Quotes']) { @@ -7752,7 +7865,7 @@ }); }, node: function() { - var ID, a, board, deadlink, m, post, quote, quoteID, redirect, _i, _len, _ref, _ref1; + var a, boardID, deadlink, m, post, postID, quote, quoteID, redirect, _i, _len, _ref, _ref1; if (this.isClone) { return; @@ -7765,33 +7878,33 @@ continue; } quote = deadlink.textContent; - if (!(ID = (_ref1 = quote.match(/\d+$/)) != null ? _ref1[0] : void 0)) { + if (!(postID = (_ref1 = quote.match(/\d+$/)) != null ? _ref1[0] : void 0)) { continue; } - board = (m = quote.match(/^>>>\/([a-z\d]+)/)) ? m[1] : this.board.ID; - quoteID = "" + board + "." + ID; + boardID = (m = quote.match(/^>>>\/([a-z\d]+)/)) ? m[1] : this.board.ID; + quoteID = "" + boardID + "." + postID; if (post = g.posts[quoteID]) { if (!post.isDead) { a = $.el('a', { - href: "/" + board + "/" + post.thread + "/res/#p" + ID, + href: "/" + boardID + "/" + post.thread + "/res/#p" + postID, className: 'quotelink', textContent: quote }); } else { a = $.el('a', { - href: "/" + board + "/" + post.thread + "/res/#p" + ID, + href: "/" + boardID + "/" + post.thread + "/res/#p" + postID, className: 'quotelink deadlink', target: '_blank', textContent: "" + quote + "\u00A0(Dead)" }); - a.setAttribute('data-board', board); + a.setAttribute('data-boardid', boardID); a.setAttribute('data-threadid', post.thread.ID); - a.setAttribute('data-postid', ID); + a.setAttribute('data-postid', postID); } } else if (redirect = Redirect.to({ - board: board, + boardID: boardID, threadID: 0, - postID: ID + postID: postID })) { a = $.el('a', { href: redirect, @@ -7799,10 +7912,10 @@ target: '_blank', textContent: "" + quote + "\u00A0(Dead)" }); - if (Redirect.post(board, ID)) { + if (Redirect.post(boardID, postID)) { $.addClass(a, 'quotelink'); - a.setAttribute('data-board', board); - a.setAttribute('data-postid', ID); + a.setAttribute('data-boardid', boardID); + a.setAttribute('data-postid', postID); } } if (!this.quotes.contains(quoteID)) { @@ -7834,35 +7947,30 @@ }); }, node: function() { - var link, _i, _j, _len, _len1, _ref, _ref1; + var link, _i, _len, _ref; - _ref = this.nodes.quotelinks; + _ref = this.nodes.quotelinks.concat(__slice.call(this.nodes.backlinks)); for (_i = 0, _len = _ref.length; _i < _len; _i++) { link = _ref[_i]; $.on(link, 'click', QuoteInline.toggle); } - _ref1 = this.nodes.backlinks; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - link = _ref1[_j]; - $.on(link, 'click', QuoteInline.toggle); - } }, toggle: function(e) { - var board, context, postID, threadID, _ref; + var boardID, context, postID, threadID, _ref; if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) { return; } e.preventDefault(); - _ref = Get.postDataFromLink(this), board = _ref.board, threadID = _ref.threadID, postID = _ref.postID; + _ref = Get.postDataFromLink(this), boardID = _ref.boardID, threadID = _ref.threadID, postID = _ref.postID; context = Get.contextFromLink(this); if ($.hasClass(this, 'inlined')) { - QuoteInline.rm(this, board, threadID, postID, context); + QuoteInline.rm(this, boardID, threadID, postID, context); } else { if ($.x("ancestor::div[@id='p" + postID + "']", this)) { return; } - QuoteInline.add(this, board, threadID, postID, context); + QuoteInline.add(this, boardID, threadID, postID, context); } return this.classList.toggle('inlined'); }, @@ -7873,8 +7981,8 @@ return $.x('ancestor-or-self::*[parent::blockquote][1]', quotelink); } }, - add: function(quotelink, board, threadID, postID, context) { - var i, inline, isBacklink, post; + add: function(quotelink, boardID, threadID, postID, context) { + var inline, isBacklink, post; isBacklink = $.hasClass(quotelink, 'backlink'); inline = $.el('div', { @@ -7882,20 +7990,20 @@ className: 'inline' }); $.after(QuoteInline.findRoot(quotelink, isBacklink), inline); - Get.postClone(board, threadID, postID, inline, context); - if (!((post = g.posts["" + board + "." + postID]) && context.thread === post.thread)) { + Get.postClone(boardID, threadID, postID, inline, context); + if (!((post = g.posts["" + boardID + "." + postID]) && context.thread === post.thread)) { return; } if (isBacklink && Conf['Forward Hiding']) { $.addClass(post.nodes.root, 'forwarded'); post.forwarded++ || (post.forwarded = 1); } - if (Unread.posts && (i = Unread.posts.indexOf(post)) !== -1) { - Unread.posts.splice(i, 1); - return Unread.update(); + if (!Unread.posts) { + return; } + return Unread.readSinglePost(post); }, - rm: function(quotelink, board, threadID, postID, context) { + rm: function(quotelink, boardID, threadID, postID, context) { var el, inlined, isBacklink, post, root, _ref; isBacklink = $.hasClass(quotelink, 'backlink'); @@ -7905,15 +8013,15 @@ if (!(el = root.firstElementChild)) { return; } - post = g.posts["" + board + "." + postID]; + post = g.posts["" + boardID + "." + postID]; post.rmClone(el.dataset.clone); - if (Conf['Forward Hiding'] && isBacklink && context.thread === g.threads["" + board + "." + threadID] && !--post.forwarded) { + if (Conf['Forward Hiding'] && isBacklink && context.thread === g.threads["" + boardID + "." + threadID] && !--post.forwarded) { delete post.forwarded; $.rmClass(post.nodes.root, 'forwarded'); } while (inlined = $('.inlined', el)) { - _ref = Get.postDataFromLink(inlined), board = _ref.board, threadID = _ref.threadID, postID = _ref.postID; - QuoteInline.rm(inlined, board, threadID, postID, context); + _ref = Get.postDataFromLink(inlined), boardID = _ref.boardID, threadID = _ref.threadID, postID = _ref.postID; + QuoteInline.rm(inlined, boardID, threadID, postID, context); $.rmClass(inlined, 'inlined'); } } @@ -7933,32 +8041,27 @@ }); }, node: function() { - var link, _i, _j, _len, _len1, _ref, _ref1; + var link, _i, _len, _ref; - _ref = this.nodes.quotelinks; + _ref = this.nodes.quotelinks.concat(__slice.call(this.nodes.backlinks)); for (_i = 0, _len = _ref.length; _i < _len; _i++) { link = _ref[_i]; $.on(link, 'mouseover', QuotePreview.mouseover); } - _ref1 = this.nodes.backlinks; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - link = _ref1[_j]; - $.on(link, 'mouseover', QuotePreview.mouseover); - } }, mouseover: function(e) { - var board, clone, origin, post, postID, posts, qp, quote, quoterID, threadID, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2; + var boardID, clone, origin, post, postID, posts, qp, quote, quoterID, threadID, _i, _j, _len, _len1, _ref, _ref1; if ($.hasClass(this, 'inlined')) { return; } - _ref = Get.postDataFromLink(this), board = _ref.board, threadID = _ref.threadID, postID = _ref.postID; + _ref = Get.postDataFromLink(this), boardID = _ref.boardID, threadID = _ref.threadID, postID = _ref.postID; qp = $.el('div', { id: 'qp', className: 'dialog' }); $.add(d.body, qp); - Get.postClone(board, threadID, postID, qp, Get.contextFromLink(this)); + Get.postClone(boardID, threadID, postID, qp, Get.contextFromLink(this)); UI.hover({ root: this, el: qp, @@ -7969,7 +8072,7 @@ return qp.firstElementChild; } }); - if (!(origin = g.posts["" + board + "." + postID])) { + if (!(origin = g.posts["" + boardID + "." + postID])) { return; } if (Conf['Quote Highlighting']) { @@ -7982,20 +8085,13 @@ } quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0]; clone = Get.postFromRoot(qp.firstChild); - _ref1 = clone.nodes.quotelinks; + _ref1 = clone.nodes.quotelinks.concat(__slice.call(clone.nodes.backlinks)); for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { quote = _ref1[_j]; if (quote.hash.slice(2) === quoterID) { $.addClass(quote, 'forwardlink'); } } - _ref2 = clone.nodes.backlinks; - for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { - quote = _ref2[_k]; - if (quote.hash.slice(2) === quoterID) { - $.addClass(quote, 'forwardlink'); - } - } }, mouseout: function() { var clone, post, root, _i, _len, _ref; @@ -8106,7 +8202,7 @@ }); }, node: function() { - var postID, quotelink, quotelinks, quotes, thread, threadID, _i, _len, _ref; + var quotelink, quotelinks, quotes, _i, _len; if (this.isClone) { return; @@ -8117,8 +8213,7 @@ quotelinks = this.nodes.quotelinks; for (_i = 0, _len = quotelinks.length; _i < _len; _i++) { quotelink = quotelinks[_i]; - _ref = Get.postDataFromLink(quotelink), threadID = _ref.threadID, postID = _ref.postID; - if ((thread = QR.yourPosts.threads[threadID]) && thread.contains(postID)) { + if (QR.db.get(Get.postDataFromLink(quotelink))) { $.add(quotelink, $.tn(QuoteYou.text)); } } @@ -8140,7 +8235,7 @@ }); }, node: function() { - var board, op, postID, quotelink, quotelinks, quotes, _i, _j, _len, _len1, _ref; + var boardID, op, postID, quotelink, quotelinks, quotes, _i, _j, _len, _len1, _ref; if (this.isClone && this.thread === this.context.thread) { return; @@ -8161,8 +8256,8 @@ } for (_j = 0, _len1 = quotelinks.length; _j < _len1; _j++) { quotelink = quotelinks[_j]; - _ref = Get.postDataFromLink(quotelink), board = _ref.board, postID = _ref.postID; - if (("" + board + "." + postID) === op) { + _ref = Get.postDataFromLink(quotelink), boardID = _ref.boardID, postID = _ref.postID; + if (("" + boardID + "." + postID) === op) { $.add(quotelink, $.tn(QuoteOP.text)); } } @@ -8184,7 +8279,7 @@ }); }, node: function() { - var board, data, quotelink, quotelinks, quotes, thread, _i, _len, _ref; + var board, boardID, quotelink, quotelinks, quotes, thread, threadID, _i, _len, _ref, _ref1; if (this.isClone && this.thread === this.context.thread) { return; @@ -8196,14 +8291,14 @@ _ref = this.isClone ? this.context : this, board = _ref.board, thread = _ref.thread; for (_i = 0, _len = quotelinks.length; _i < _len; _i++) { quotelink = quotelinks[_i]; - data = Get.postDataFromLink(quotelink); - if (!data.threadID) { + _ref1 = Get.postDataFromLink(quotelink), boardID = _ref1.boardID, threadID = _ref1.threadID; + if (!threadID) { continue; } if (this.isClone) { quotelink.textContent = quotelink.textContent.replace(QuoteCT.text, ''); } - if (data.board === this.board.ID && data.threadID !== thread.ID) { + if (boardID === this.board.ID && threadID !== thread.ID) { $.add(quotelink, $.tn(QuoteCT.text)); } } @@ -8603,7 +8698,7 @@ }); this.EAI = wrapper.firstElementChild; $.on(this.EAI, 'click', ImageExpand.cb.toggleAll); - this.menu = new UI.Menu('imageexpand'); + this.opmenu = new UI.Menu('imageexpand'); $.on($('.menu-button', wrapper), 'click', this.menuToggle); _ref = Config.imageExpansion; for (type in _ref) { @@ -8703,27 +8798,27 @@ } }, toggle: function(post) { - var headRect, postRect, rect, root, thumb, top; + var headRect, rect, root, thumb, top; thumb = post.file.thumb; if (!(post.file.isExpanded || $.hasClass(thumb, 'expanding'))) { ImageExpand.expand(post); return; } - rect = thumb.parentNode.getBoundingClientRect(); - if (rect.bottom > 0) { - postRect = post.nodes.root.getBoundingClientRect(); - headRect = Header.toggle.getBoundingClientRect(); - top = postRect.top - headRect.top - headRect.height - 2; - root = $.engine === 'webkit' ? d.body : doc; - if (rect.top < 0) { - root.scrollTop += top; - } - if (rect.left < 0) { - root.scrollLeft = 0; - } + ImageExpand.contract(post); + rect = post.nodes.root.getBoundingClientRect(); + if (!(rect.top <= 0 || rect.left <= 0)) { + return; + } + headRect = Header.bar.getBoundingClientRect(); + top = rect.top - headRect.top - headRect.height; + root = $.engine === 'webkit' ? d.body : doc; + if (rect.top < 0) { + root.scrollTop += top; + } + if (rect.left < 0) { + return root.scrollLeft = 0; } - return ImageExpand.contract(post); }, contract: function(post) { $.rmClass(post.nodes.root, 'expanded-image'); @@ -8759,20 +8854,31 @@ return $.after(thumb, img); }, completeExpand: function(post) { - var rect, root, thumb; + var prev, thumb; thumb = post.file.thumb; if (!$.hasClass(thumb, 'expanding')) { return; } - rect = post.nodes.root.getBoundingClientRect(); - $.addClass(post.nodes.root, 'expanded-image'); - $.rmClass(post.file.thumb, 'expanding'); - if (rect.top + rect.height <= 0) { - root = $.engine === 'webkit' ? d.body : doc; - root.scrollTop += post.nodes.root.clientHeight - rect.height; + post.file.isExpanded = true; + if (!post.nodes.root.parentNode) { + $.addClass(post.nodes.root, 'expanded-image'); + $.rmClass(post.file.thumb, 'expanding'); + return; } - return post.file.isExpanded = true; + prev = post.nodes.root.getBoundingClientRect(); + return $.queueTask(function() { + var curr, root; + + $.addClass(post.nodes.root, 'expanded-image'); + $.rmClass(post.file.thumb, 'expanding'); + if (!(prev.top + prev.height <= 0)) { + return; + } + root = $.engine === 'webkit' ? d.body : doc; + curr = post.nodes.root.getBoundingClientRect(); + return root.scrollTop += curr.height - prev.height + curr.top - prev.top; + }); }, error: function() { var URL, post, src, timeoutID; @@ -8819,11 +8925,57 @@ } }); }, + menu: { + init: function() { + var conf, createSubEntry, el, key, subEntries, _ref; + + if (g.VIEW === 'catalog' || !Conf['Image Expansion']) { + return; + } + el = $.el('span', { + textContent: 'Image Expansion', + className: 'image-expansion-link' + }); + createSubEntry = ImageExpand.menu.createSubEntry; + subEntries = []; + _ref = Config.imageExpansion; + for (key in _ref) { + conf = _ref[key]; + subEntries.push(createSubEntry(key, conf)); + } + return $.event('AddMenuEntry', { + type: 'header', + el: el, + order: 80, + subEntries: subEntries + }); + }, + createSubEntry: function(type, config) { + var input, label; + + label = $.el('label', { + innerHTML: " " + type + }); + input = label.firstElementChild; + if (type === 'Fit width' || type === 'Fit height') { + $.on(input, 'change', ImageExpand.cb.setFitness); + } + if (config) { + label.title = config[1]; + input.checked = Conf[type]; + $.event('change', null, input); + $.on(input, 'change', $.cb.checked); + } + return { + el: label + }; + } + }, resize: function() { return ImageExpand.style.textContent = ":root.fit-height .full-image {max-height:" + doc.clientHeight + "px}"; }, menuToggle: function(e) { - return ImageExpand.menu.toggle(e, this, g); + return ImageExpand.opmenu.toggle(e, this, g); } }; @@ -9105,54 +9257,66 @@ return ExpandThread.toggle(op.thread); }, toggle: function(thread) { - var a, inlined, num, post, replies, reply, text, threadRoot, url, _i, _j, _k, _len, _len1, _len2, _ref, _ref1; + var a, inlined, num, post, replies, reply, threadRoot, _i, _j, _k, _len, _len1, _len2, _ref, _ref1; threadRoot = thread.OP.nodes.root.parentNode; - url = "//api.4chan.org/" + thread.board + "/res/" + thread + ".json"; a = $('.summary', threadRoot); - text = a.textContent; - switch (text[0]) { - case '+': - a.textContent = text.replace('+', '× Loading...'); - $.cache(url, function() { - return ExpandThread.parse(this, thread, a); - }); + switch (thread.isExpanded) { + case false: + case void 0: + thread.isExpanded = 'loading'; _ref = $$('.thread > .postContainer', threadRoot); for (_i = 0, _len = _ref.length; _i < _len; _i++) { post = _ref[_i]; ExpandComment.expand(Get.postFromRoot(post)); } + if (!a) { + thread.isExpanded = true; + return; + } + thread.isExpanded = 'loading'; + a.textContent = a.textContent.replace('+', '× Loading...'); + $.cache("//api.4chan.org/" + thread.board + "/res/" + thread + ".json", function() { + return ExpandThread.parse(this, thread, a); + }); break; - case '×': - a.textContent = text.replace('× Loading...', '+'); + case 'loading': + thread.isExpanded = false; + if (!a) { + return; + } + a.textContent = a.textContent.replace('× Loading...', '+'); break; - case '-': - a.textContent = text.replace('-', '+'); - num = (function() { - if (thread.isSticky) { - return 1; - } else { - switch (g.BOARD.ID) { - case 'b': - case 'vg': - case 'q': - return 3; - case 't': - return 1; - default: - return 5; + case true: + thread.isExpanded = false; + if (a) { + a.textContent = a.textContent.replace('-', '+'); + num = (function() { + if (thread.isSticky) { + return 1; + } else { + switch (g.BOARD.ID) { + case 'b': + case 'vg': + case 'q': + return 3; + case 't': + return 1; + default: + return 5; + } } - } - })(); - replies = $$('.thread > .replyContainer', threadRoot).slice(0, -num); - for (_j = 0, _len1 = replies.length; _j < _len1; _j++) { - reply = replies[_j]; - if (Conf['Quote Inlining']) { - while (inlined = $('.inlined', reply)) { - inlined.click(); + })(); + replies = $$('.thread > .replyContainer', threadRoot).slice(0, -num); + for (_j = 0, _len1 = replies.length; _j < _len1; _j++) { + reply = replies[_j]; + if (Conf['Quote Inlining']) { + while (inlined = $('.inlined', reply)) { + inlined.click(); + } } + $.rm(reply); } - $.rm(reply); } _ref1 = $$('.thread > .postContainer', threadRoot); for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { @@ -9173,6 +9337,7 @@ $.off(a, 'click', ExpandThread.cb.toggle); return; } + thread.isExpanded = true; a.textContent = a.textContent.replace('× Loading...', '-'); posts = JSON.parse(req.response).posts; if (spoilerRange = posts[0].custom_spoiler) { @@ -9227,24 +9392,21 @@ if (g.VIEW !== 'thread' || !Conf['Unread Count'] && !Conf['Unread Tab Icon']) { return; } - Unread.hr = $.el('hr', { + this.db = new DataBoard('lastReadPosts', this.sync); + this.hr = $.el('hr', { id: 'unread-line' }); - Misc.clearThreads("lastReadPosts." + g.BOARD); + this.posts = []; + this.postsQuotingYou = []; return Thread.prototype.callbacks.push({ name: 'Unread', cb: this.node }); }, node: function() { - var ID, post, posts, _ref; + var ID, hash, post, posts, _ref; Unread.thread = this; - Unread.lastReadPost = $.get("lastReadPosts." + this.board, { - threads: {} - }).threads[this] || 0; - Unread.posts = []; - Unread.postsQuotingYou = []; Unread.title = d.title; posts = []; _ref = this.posts; @@ -9254,8 +9416,15 @@ posts.push(post); } } + Unread.lastReadPost = Unread.db.get({ + boardID: this.board.ID, + threadID: this.ID, + defaultValue: 0 + }); Unread.addPosts(posts); - if (Unread.posts.length) { + if ((hash = location.hash.match(/\d+/)) && (post = this.posts[hash[0]])) { + post.nodes.root.scrollIntoView(); + } else if (Unread.posts.length) { $.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView(false); } else if (posts.length) { posts[posts.length - 1].nodes.root.scrollIntoView(); @@ -9266,23 +9435,44 @@ return $.on(d, 'visibilitychange', Unread.setLine); } }, - addPosts: function(newPosts) { - var ID, post, youInThisThread, yourPosts, _i, _len; + sync: function() { + var lastReadPost; - if (Conf['Quick Reply']) { - yourPosts = QR.yourPosts; - youInThisThread = yourPosts.threads[Unread.thread]; + lastReadPost = Unread.db.get({ + boardID: Unread.thread.board.ID, + threadID: Unread.thread.ID, + defaultValue: 0 + }); + if (!(Unread.lastReadPost < lastReadPost)) { + return; } + Unread.lastReadPost = lastReadPost; + Unread.readArray(Unread.posts); + Unread.readArray(Unread.postsQuotingYou); + Unread.setLine(); + return Unread.update(); + }, + addPosts: function(newPosts) { + var ID, data, post, _i, _len; + for (_i = 0, _len = newPosts.length; _i < _len; _i++) { post = newPosts[_i]; ID = post.ID; - if (ID <= Unread.lastReadPost || post.isHidden || youInThisThread && youInThisThread.contains(ID)) { + if (ID <= Unread.lastReadPost || post.isHidden) { continue; } - Unread.posts.push(post); - if (yourPosts) { - Unread.addPostQuotingYou(post, yourPosts); + if (QR.db) { + data = { + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + }; + if (QR.db.get(data)) { + continue; + } } + Unread.posts.push(post); + Unread.addPostQuotingYou(post); } if (Conf['Unread Line']) { Unread.setLine(newPosts.contains(Unread.posts[0])); @@ -9290,23 +9480,17 @@ Unread.read(); return Unread.update(); }, - addPostQuotingYou: function(post, yourPosts) { - var board, postIDs, quote, quoteID, thread, _i, _len, _ref, _ref1, _ref2; + addPostQuotingYou: function(post) { + var quotelink, _i, _len, _ref; - _ref = post.quotes; + if (!QR.db) { + return; + } + _ref = post.nodes.quotelinks; for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - _ref1 = quote.split('.'), board = _ref1[0], quoteID = _ref1[1]; - if (board !== Unread.thread.board.ID) { - continue; - } - _ref2 = yourPosts.threads; - for (thread in _ref2) { - postIDs = _ref2[thread]; - if (postIDs.contains(+quoteID)) { - Unread.postsQuotingYou.push(post); - return; - } + quotelink = _ref[_i]; + if (QR.db.get(Get.postDataFromLink(quotelink))) { + Unread.postsQuotingYou.push(post); } } }, @@ -9317,8 +9501,35 @@ return Unread.addPosts(e.detail.newPosts); } }, + readSinglePost: function(post) { + var i; + + if ((i = Unread.posts.indexOf(post)) === -1) { + return; + } + Unread.posts.splice(i, 1); + if (i === 0) { + Unread.lastReadPost = post.ID; + Unread.saveLastReadPost(); + } + if ((i = Unread.postsQuotingYou.indexOf(post)) !== -1) { + Unread.postsQuotingYou.splice(i, 1); + } + return Unread.update(); + }, + readArray: function(arr) { + var i, post, _i, _len; + + for (i = _i = 0, _len = arr.length; _i < _len; i = ++_i) { + post = arr[i]; + if (post.ID > Unread.lastReadPost) { + break; + } + } + return arr.splice(0, i); + }, read: function(e) { - var bottom, height, i, post, _i, _j, _len, _len1, _ref, _ref1; + var bottom, height, i, post, _i, _len, _ref; if (d.hidden || !Unread.posts.length) { return; @@ -9337,27 +9548,18 @@ } Unread.lastReadPost = Unread.posts[i - 1].ID; Unread.saveLastReadPost(); - Unread.posts = Unread.posts.slice(i); - _ref1 = Unread.postsQuotingYou; - for (i = _j = 0, _len1 = _ref1.length; _j < _len1; i = ++_j) { - post = _ref1[i]; - if (post.ID > Unread.lastReadPost) { - break; - } - } - Unread.postsQuotingYou = Unread.postsQuotingYou.slice(i); + Unread.posts.splice(0, i); + Unread.readArray(Unread.postsQuotingYou); if (e) { return Unread.update(); } }, - saveLastReadPost: $.debounce($.SECOND, function() { - var lastReadPosts; - - lastReadPosts = $.get("lastReadPosts." + Unread.thread.board, { - threads: {} + saveLastReadPost: $.debounce(2 * $.SECOND, function() { + return Unread.db.set({ + boardID: Unread.thread.board.ID, + threadID: Unread.thread.ID, + val: Unread.lastReadPost }); - lastReadPosts.threads[Unread.thread] = Unread.lastReadPost; - return $.set("lastReadPosts." + Unread.thread.board, lastReadPosts); }), setLine: function(force) { var post, root; @@ -9384,7 +9586,7 @@ if (!Conf['Unread Tab Icon']) { return; } - Favicon.el.href = g.DEAD ? Unread.postsQuotingYou.length ? Favicon.unreadDeadY : count ? Favicon.unreadDead : Favicon.dead : Unread.postsQuotingYou.length ? Favicon.unreadY : count ? Favicon.unread : Favicon["default"]; + Favicon.el.href = g.DEAD ? Unread.postsQuotingYou.length ? Favicon.unreadDeadY : count ? Favicon.unreadDead : Favicon.dead : count ? Unread.postsQuotingYou.length ? Favicon.unreadY : Favicon.unread : Favicon["default"]; return $.add(d.head, Favicon.el); } }; @@ -9474,25 +9676,28 @@ fileCount++; } } - ThreadStats.update(postCount, fileCount); ThreadStats.thread = this; + ThreadStats.update(postCount, fileCount); $.on(d, 'ThreadUpdate', ThreadStats.onUpdate); return $.add(d.body, ThreadStats.dialog); }, onUpdate: function(e) { - var fileCount, fileLimit, postCount, postLimit, _ref; + var fileCount, postCount, _ref; if (e.detail[404]) { return; } - _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount, postLimit = _ref.postLimit, fileLimit = _ref.fileLimit; - return ThreadStats.update(postCount, fileCount, postLimit, fileLimit); + _ref = e.detail, postCount = _ref.postCount, fileCount = _ref.fileCount; + return ThreadStats.update(postCount, fileCount); }, - update: function(postCount, fileCount, postLimit, fileLimit) { - ThreadStats.postCountEl.textContent = postCount; - ThreadStats.fileCountEl.textContent = fileCount; - (postLimit && !ThreadStats.thread.isSticky ? $.addClass : $.rmClass)(ThreadStats.postCountEl, 'warning'); - return (fileLimit && !ThreadStats.thread.isSticky ? $.addClass : $.rmClass)(ThreadStats.fileCountEl, 'warning'); + update: function(postCount, fileCount) { + var fileCountEl, postCountEl, thread; + + thread = ThreadStats.thread, postCountEl = ThreadStats.postCountEl, fileCountEl = ThreadStats.fileCountEl; + postCountEl.textContent = postCount; + fileCountEl.textContent = fileCount; + (thread.postLimit && !thread.isSticky ? $.addClass : $.rmClass)(postCountEl, 'warning'); + return (thread.fileLimit && !thread.isSticky ? $.addClass : $.rmClass)(fileCountEl, 'warning'); } }; @@ -9757,6 +9962,8 @@ Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler; ThreadUpdater.updateThreadStatus('Sticky', OP); ThreadUpdater.updateThreadStatus('Closed', OP); + ThreadUpdater.thread.postLimit = !!OP.bumplimit; + ThreadUpdater.thread.fileLimit = !!OP.imagelimit; nodes = []; posts = []; index = []; @@ -9842,9 +10049,7 @@ deletedPosts: deletedPosts, deletedFiles: deletedFiles, postCount: OP.replies + 1, - fileCount: OP.images + (!!ThreadUpdater.thread.OP.file && !ThreadUpdater.thread.OP.file.isDead), - postLimit: !!OP.bumplimit, - fileLimit: !!OP.imagelimit + fileCount: OP.images + (!!ThreadUpdater.thread.OP.file && !ThreadUpdater.thread.OP.file.isDead) }); } }; @@ -9864,17 +10069,24 @@ }); }, node: function() { - var favicon; + var favicon, + _this = this; favicon = $.el('img', { className: 'favicon' }); $.on(favicon, 'click', ThreadWatcher.cb.toggle); $.before($('input', this.OP.nodes.post), favicon); - if (g.VIEW === 'thread' && this.ID === $.get('AutoWatch', 0)) { - ThreadWatcher.watch(this); - return $["delete"]('AutoWatch'); + if (g.VIEW !== 'thread') { + return; } + return $.get('AutoWatch', 0, function(item) { + if (item['AutoWatch'] !== _this.ID) { + return; + } + ThreadWatcher.watch(_this); + return $["delete"]('AutoWatch'); + }); }, ready: function() { if (!Main.isThisPageLegit()) { @@ -9886,7 +10098,12 @@ refresh: function(watched) { var ID, board, div, favicon, id, link, nodes, props, thread, x, _ref, _ref1; - watched || (watched = $.get('WatchedThreads', {})); + if (!watched) { + $.get('WatchedThreads', {}, function(item) { + return ThreadWatcher.refresh(item['WatchedThreads']); + }); + return; + } nodes = [$('.move', ThreadWatcher.dialog)]; for (board in watched) { _ref = watched[board]; @@ -9945,27 +10162,31 @@ } }, unwatch: function(board, threadID) { - var watched; + return $.get('WatchedThreads', {}, function(item) { + var watched; - watched = $.get('WatchedThreads', {}); - delete watched[board][threadID]; - if (!Object.keys(watched[board]).length) { - delete watched[board]; - } - ThreadWatcher.refresh(watched); - return $.set('WatchedThreads', watched); + watched = item['WatchedThreads']; + delete watched[board][threadID]; + if (!Object.keys(watched[board]).length) { + delete watched[board]; + } + ThreadWatcher.refresh(watched); + return $.set('WatchedThreads', watched); + }); }, watch: function(thread) { - var watched, _name; + return $.get('WatchedThreads', {}, function(item) { + var watched, _name; - watched = $.get('WatchedThreads', {}); - watched[_name = thread.board] || (watched[_name] = {}); - watched[thread.board][thread] = { - href: "/" + thread.board + "/res/" + thread, - textContent: Get.threadExcerpt(thread) - }; - ThreadWatcher.refresh(watched); - return $.set('WatchedThreads', watched); + watched = item['WatchedThreads']; + watched[_name = thread.board] || (watched[_name] = {}); + watched[thread.board][thread] = { + href: "/" + thread.board + "/res/" + thread, + textContent: Get.threadExcerpt(thread) + }; + ThreadWatcher.refresh(watched); + return $.set('WatchedThreads', watched); + }); } }; @@ -10239,11 +10460,10 @@ init: function() { var sc; - if (g.VIEW === 'catalog' || !Conf['Quick Reply']) { + if (!Conf['Quick Reply']) { return; } - Misc.clearThreads("yourPosts." + g.BOARD); - this.syncYourPosts(); + this.db = new DataBoard('yourPosts'); sc = $.el('a', { className: "qr-shortcut " + (!Conf['Persistent QR'] ? 'disabled' : ''), textContent: 'QR', @@ -10362,16 +10582,6 @@ return QR.unhide(); } }, - syncYourPosts: function(yourPosts) { - if (yourPosts) { - QR.yourPosts = yourPosts; - return; - } - QR.yourPosts = $.get("yourPosts." + g.BOARD, { - threads: {} - }); - return $.sync("yourPosts." + g.BOARD, QR.syncYourPosts); - }, error: function(err) { var el; @@ -10439,10 +10649,12 @@ file: board === 'q' ? 300 : 30, post: board === 'q' ? 60 : 30 }; - QR.cooldown.cooldowns = $.get("cooldown." + board, {}); QR.cooldown.upSpd = 0; QR.cooldown.upSpdAccuracy = .5; - QR.cooldown.start(); + $.get("cooldown." + board, {}, function(item) { + QR.cooldown.cooldowns = item["cooldown." + board]; + return QR.cooldown.start(); + }); return $.sync("cooldown." + board, QR.cooldown.sync); }, start: function() { @@ -10683,24 +10895,17 @@ }, resetThreadSelector: function() { if (g.VIEW === 'thread') { - return QR.nodes.thread.value = g.THREAD; + return QR.nodes.thread.value = g.THREADID; } else { return QR.nodes.thread.value = 'new'; } }, posts: [], post: (function() { - function _Class() { - var el, event, persona, prev, _i, _len, _ref, + function _Class(select) { + var el, event, prev, _i, _len, _ref, _this = this; - prev = QR.posts[QR.posts.length - 1]; - persona = $.get('QR.persona', {}); - this.name = prev ? prev.name : persona.name || null; - this.email = prev && !/^sage$/.test(prev.email) ? prev.email : persona.email || null; - this.sub = prev && Conf['Remember Subject'] ? prev.sub : Conf['Remember Subject'] ? persona.sub : null; - this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false; - this.com = null; el = $.el('a', { className: 'qr-preview', draggable: true, @@ -10735,8 +10940,26 @@ event = _ref[_i]; $.on(el, event.toLowerCase(), this[event]); } - this.unlock(); + prev = QR.posts[QR.posts.length - 1]; QR.posts.push(this); + this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false; + $.get('QR.persona', {}, function(item) { + var persona; + + persona = item['QR.persona']; + _this.name = prev ? prev.name : persona.name; + _this.email = prev && !/^sage$/.test(prev.email) ? prev.email : persona.email; + if (Conf['Remember Subject']) { + _this.sub = prev ? prev.sub : persona.sub; + } + if (QR.selected === _this) { + return _this.load(); + } + }); + if (select) { + this.select(); + } + this.unlock(); } _Class.prototype.rm = function() { @@ -10745,7 +10968,7 @@ $.rm(this.nodes.el); index = QR.posts.indexOf(this); if (QR.posts.length === 1) { - new QR.post().select(); + new QR.post(true); } else if (this === QR.selected) { (QR.posts[index - 1] || QR.posts[index + 1]).select(); } @@ -10782,7 +11005,7 @@ }; _Class.prototype.select = function() { - var name, rectEl, rectList, _i, _len, _ref; + var rectEl, rectList; if (QR.selected) { QR.selected.nodes.el.id = null; @@ -10794,10 +11017,16 @@ rectEl = this.nodes.el.getBoundingClientRect(); rectList = this.nodes.el.parentNode.getBoundingClientRect(); this.nodes.el.parentNode.scrollLeft += rectEl.left + rectEl.width / 2 - rectList.left - rectList.width / 2; + return this.load(); + }; + + _Class.prototype.load = function() { + var name, _i, _len, _ref; + _ref = ['name', 'email', 'sub', 'com']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { name = _ref[_i]; - QR.nodes[name].value = this[name]; + QR.nodes[name].value = this[name] || null; } this.showFileData(); return QR.characterCount(); @@ -11034,7 +11263,6 @@ $.on(window, 'captcha:timeout', setLifetime); $.globalEval('window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'); $.off(window, 'captcha:timeout', setLifetime); - c.log(this.lifetime); imgContainer = $.el('div', { className: 'captcha-img', title: 'Reload', @@ -11061,8 +11289,10 @@ } $.on(imgContainer, 'click', this.reload.bind(this)); $.on(input, 'keydown', this.keydown.bind(this)); + $.get('captchas', [], function(item) { + return _this.sync(item['captchas']); + }); $.sync('captchas', this.sync); - this.sync($.get('captchas', [])); this.reload(); $.addClass(QR.nodes.el, 'has-captcha'); return $.after(QR.nodes.com.parentNode, [imgContainer, input]); @@ -11248,7 +11478,7 @@ return nodes.el.classList.toggle('dump'); }); $.on(nodes.addPost, 'click', function() { - return new QR.post().select(); + return new QR.post(true); }); $.on(nodes.form, 'submit', QR.submit); $.on(nodes.fileRM, 'click', function() { @@ -11258,7 +11488,7 @@ return QR.selected.nodes.spoiler.click(); }); $.on(nodes.fileInput, 'change', QR.fileInput); - new QR.post().select(); + new QR.post(true); _ref1 = ['name', 'email', 'sub', 'com']; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { name = _ref1[_j]; @@ -11300,10 +11530,12 @@ } else if (!(post.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) { err = 'No file selected.'; } - } else if (g.BOARD.threads[threadID].isSticky) { + } else if (g.BOARD.threads[threadID].isClosed) { err = 'You can\'t reply to this thread anymore.'; } else if (!(post.com || post.file)) { err = 'No file selected.'; + } else if (post.file && g.BOARD.threads[threadID].fileLimit) { + err = 'Max limit of image replies has been reached.'; } if (QR.captcha.isEnabled && !err) { _ref = QR.captcha.getOne(), challenge = _ref.challenge, response = _ref.response; @@ -11354,6 +11586,7 @@ } }; opts = { + cred: true, form: $.formData(postData), upCallbacks: { onload: function() { @@ -11374,7 +11607,7 @@ return QR.status(); }, response: function() { - var URL, ban, board, err, h1, isReply, persona, post, postID, req, threadID, tmpDoc, _, _base, _ref, _ref1; + var URL, ban, board, err, h1, isReply, post, postID, req, threadID, tmpDoc, _, _ref, _ref1; req = QR.req; delete QR.req; @@ -11415,19 +11648,27 @@ h1 = $('h1', tmpDoc); QR.cleanNotifications(); QR.notifications.push(new Notification('success', h1.textContent, 5)); - persona = $.get('QR.persona', {}); - persona = { - name: post.name, - email: /^sage$/.test(post.email) ? persona.email : post.email, - sub: Conf['Remember Subject'] ? post.sub : null - }; - $.set('QR.persona', persona); + $.get('QR.persona', {}, function(item) { + var persona; + + persona = item['QR.persona']; + persona = { + name: post.name, + email: /^sage$/.test(post.email) ? persona.email : post.email, + sub: Conf['Remember Subject'] ? post.sub : null + }; + return $.set('QR.persona', persona); + }); _ref1 = h1.nextSibling.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref1[0], threadID = _ref1[1], postID = _ref1[2]; postID = +postID; threadID = +threadID || postID; isReply = threadID !== postID; - ((_base = QR.yourPosts.threads)[threadID] || (_base[threadID] = [])).push(postID); - $.set("yourPosts." + g.BOARD, QR.yourPosts); + QR.db.set({ + boardID: g.BOARD.ID, + threadID: threadID, + postID: postID, + val: true + }); ThreadUpdater.postID = postID; $.event('QRPostSuccessful', { board: g.BOARD, @@ -11435,7 +11676,11 @@ postID: postID }, QR.nodes.el); QR.cooldown.auto = QR.posts.length > 1 && isReply; - post.rm(); + if (!(Conf['Persistent QR'] || QR.cooldown.auto)) { + QR.close(); + } else { + post.rm(); + } QR.cooldown.set({ req: req, post: post, @@ -11453,9 +11698,6 @@ window.location = "/" + g.BOARD + "/res/" + threadID; } } - if (!(Conf['Persistent QR'] || QR.cooldown.auto)) { - QR.close(); - } return QR.status(); }, abort: function() { @@ -11499,6 +11741,159 @@ } }; + DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts']; + + DataBoard = (function() { + function DataBoard(key, sync) { + var _this = this; + + this.key = key; + this.data = Conf[key]; + $.sync(key, this.onSync.bind(this)); + this.clean(); + if (!sync) { + return; + } + $.on(d, '4chanXInitFinished', function() { + return _this.sync = sync; + }); + } + + DataBoard.prototype["delete"] = function(_arg) { + var boardID, postID, threadID; + + boardID = _arg.boardID, threadID = _arg.threadID, postID = _arg.postID; + if (postID) { + delete this.data.boards[boardID][threadID][postID]; + this.deleteIfEmpty({ + boardID: boardID, + threadID: threadID + }); + } else if (threadID) { + delete this.data.boards[boardID][threadID]; + this.deleteIfEmpty({ + boardID: boardID + }); + } else { + delete this.data.boards[boardID]; + } + return $.set(this.key, this.data); + }; + + DataBoard.prototype.deleteIfEmpty = function(_arg) { + var boardID, threadID; + + boardID = _arg.boardID, threadID = _arg.threadID; + if (threadID) { + if (!Object.keys(this.data.boards[boardID][threadID]).length) { + delete this.data.boards[boardID][threadID]; + return this.deleteIfEmpty({ + boardID: boardID + }); + } + } else if (!Object.keys(this.data.boards[boardID]).length) { + return delete this.data.boards[boardID]; + } + }; + + DataBoard.prototype.set = function(_arg) { + var boardID, postID, threadID, val, _base, _base1, _base2; + + boardID = _arg.boardID, threadID = _arg.threadID, postID = _arg.postID, val = _arg.val; + if (postID) { + ((_base = ((_base1 = this.data.boards)[boardID] || (_base1[boardID] = {})))[threadID] || (_base[threadID] = {}))[postID] = val; + } else if (threadID) { + ((_base2 = this.data.boards)[boardID] || (_base2[boardID] = {}))[threadID] = val; + } else { + this.data.boards[boardID] = val; + } + return $.set(this.key, this.data); + }; + + DataBoard.prototype.get = function(_arg) { + var ID, board, boardID, defaultValue, postID, thread, threadID, val, _i, _len; + + boardID = _arg.boardID, threadID = _arg.threadID, postID = _arg.postID, defaultValue = _arg.defaultValue; + if (board = this.data.boards[boardID]) { + if (!threadID) { + if (postID) { + for (thread = _i = 0, _len = board.length; _i < _len; thread = ++_i) { + ID = board[thread]; + if (postID in thread) { + val = thread[postID]; + break; + } + } + } else { + val = board; + } + } else if (thread = board[threadID]) { + val = postID ? thread[postID] : thread; + } + } + return val || defaultValue; + }; + + DataBoard.prototype.clean = function() { + var boardID, now; + + for (boardID in this.data.boards) { + this.deleteIfEmpty({ + boardID: boardID + }); + } + now = Date.now(); + if ((this.data.lastChecked || 0) < now - 12 * $.HOUR) { + this.data.lastChecked = now; + for (boardID in this.data.boards) { + this.ajaxClean(boardID); + } + } + return $.set(this.key, this.data); + }; + + DataBoard.prototype.ajaxClean = function(boardID) { + var _this = this; + + return $.cache("//api.4chan.org/" + boardID + "/threads.json", function(e) { + var board, page, thread, threads, _i, _j, _len, _len1, _ref, _ref1; + + if (e.target.status === 404) { + _this["delete"](boardID); + } else if (e.target.status === 200) { + board = _this.data.boards[boardID]; + threads = {}; + _ref = JSON.parse(e.target.response); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + page = _ref[_i]; + _ref1 = page.threads; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + thread = _ref1[_j]; + if (thread.no in board) { + threads[thread.no] = board[thread.no]; + } + } + } + _this.data.boards[boardID] = threads; + _this.deleteIfEmpty({ + boardID: boardID + }); + } + return $.set(_this.key, _this.data); + }); + }; + + DataBoard.prototype.onSync = function(data) { + this.data = data || { + boards: {} + }; + return typeof this.sync === "function" ? this.sync() : void 0; + }; + + return DataBoard; + + })(); + Board = (function() { Board.prototype.toString = function() { return this.ID; @@ -11527,7 +11922,7 @@ this.ID = +ID; this.fullID = "" + this.board + "." + this.ID; this.posts = {}; - g.threads["" + board + "." + this] = board.threads[this] = this; + g.threads[this.fullID] = board.threads[this] = this; } Thread.prototype.kill = function() { @@ -11547,7 +11942,7 @@ }; function Post(root, thread, board, that) { - var alt, anchor, capcode, date, email, file, fileInfo, flag, info, name, post, size, subject, thumb, tripcode, uniqueID, unit, _ref; + var alt, anchor, capcode, date, email, file, fileInfo, flag, info, name, post, size, subject, thumb, tripcode, uniqueID, unit; this.thread = thread; this.board = board; @@ -11600,7 +11995,11 @@ this.info.date = new Date(date.dataset.utc * 1000); } if (Conf['Quick Reply']) { - this.info.yours = !!((_ref = QR.yourPosts.threads[this.thread.ID]) != null ? _ref.contains(this.ID) : void 0); + this.info.yours = QR.db.get({ + boardID: this.board, + threadID: this.thread, + postID: this.ID + }); } this.parseComment(); this.parseQuotes(); @@ -11635,7 +12034,7 @@ this.thread.isClosed = !!$('.closedIcon', this.nodes.info); } this.clones = []; - g.posts["" + board + "." + this] = thread.posts[this] = board.posts[this] = this; + g.posts[this.fullID] = thread.posts[this] = board.posts[this] = this; if (that.isArchived) { this.kill(); } @@ -11693,10 +12092,16 @@ now || (now = new Date()); if (file) { + if (this.file.isDead) { + return; + } this.file.isDead = true; this.file.timeOfDeath = now; $.addClass(this.nodes.root, 'deleted-file'); } else { + if (this.isDead) { + return; + } this.isDead = true; this.timeOfDeath = now; $.addClass(this.nodes.root, 'deleted-post'); @@ -11869,8 +12274,8 @@ })(Post); Main = { - init: function() { - var flatten, initFeatures, key, pathname, val; + init: function(items) { + var db, flatten, _i, _len; flatten = function(parent, obj) { var key, val; @@ -11887,10 +12292,19 @@ } }; flatten(null, Config); - for (key in Conf) { - val = Conf[key]; - Conf[key] = $.get(key, val); + for (_i = 0, _len = DataBoards.length; _i < _len; _i++) { + db = DataBoards[_i]; + Conf[db] = { + boards: {} + }; } + $.get(Conf, Main.initFeatures); + return $.on(d, '4chanMainInit', Main.initStyle); + }, + initFeatures: function(items) { + var initFeatures, pathname; + + Conf = items; pathname = location.pathname.split('/'); g.BOARD = new Board(pathname[1]); g.VIEW = (function() { @@ -11904,7 +12318,7 @@ } })(); if (g.VIEW === 'thread') { - g.THREAD = +pathname[3]; + g.THREADID = +pathname[3]; } if (['b', 'd', 'e', 'gif', 'h', 'hc', 'hm', 'hr', 'pol', 'r', 'r9k', 'rs', 's', 'soc', 't', 'u', 'y'].contains(g.BOARD)) { g.TYPE = 'nsfw'; @@ -11967,14 +12381,14 @@ 'Resurrect Quotes': Quotify, 'Filter': Filter, 'Thread Hiding': ThreadHiding, - 'Reply Hiding': ReplyHiding, + 'Reply Hiding': PostHiding, 'Recursive': Recursive, 'Strike-through Quotes': QuoteStrikeThrough, 'Quick Reply': QR, 'Menu': Menu, 'Report Link': ReportLink, 'Thread Hiding (Menu)': ThreadHiding.menu, - 'Reply Hiding (Menu)': ReplyHiding.menu, + 'Reply Hiding (Menu)': PostHiding.menu, 'Delete Link': DeleteLink, 'Filter (Menu)': Filter.menu, 'Download Link': DownloadLink, @@ -11991,6 +12405,7 @@ 'File Info Formatting': FileInfo, 'Sauce': Sauce, 'Image Expansion': ImageExpand, + 'Image Expansion (Menu)': ImageExpand.menu, 'Reveal Spoilers': RevealSpoilers, 'Image Replace': ImageReplace, 'Image Hover': ImageHover, @@ -12014,9 +12429,9 @@ if (d.title === '4chan - 404 Not Found') { if (Conf['404 Redirect'] && g.VIEW === 'thread') { href = Redirect.to({ - board: g.BOARD, - threadID: g.THREAD, - postID: location.hash + boardID: g.BOARD.ID, + threadID: g.THREADID, + postID: +location.hash.match(/\d+/) }); location.href = href || ("/" + g.BOARD + "/"); } @@ -12110,43 +12525,49 @@ return Klass.prototype.callbacks.push(obj.callback); }, checkUpdate: function() { - var freq, now; + var freq, items, now; - if (!Main.isThisPageLegit()) { + if (!(Conf['Check for Updates'] && Main.isThisPageLegit())) { return; } now = Date.now(); freq = 7 * $.DAY; - if ($.get('lastupdate', 0) > now - freq || $.get('lastchecked', 0) > now - $.DAY) { - return; - } - return $.ajax('http://zixaphir.github.com/appchan-x/builds/version', { - onload: function() { - var el, version; - - if (this.status !== 200) { - return; - } - version = this.response; - if (!/^\d\.\d+\.\d+$/.test(version)) { - return; - } - if (g.VERSION === version) { - $.set('lastupdate', now); - return; - } - $.set('lastchecked', now); - el = $.el('span', { - innerHTML: "Update: appchan x v" + version + " is out, get it here." - }); - return new Notification('info', el, 2 * $.MINUTE); + items = { + lastupdate: 0, + lastchecked: 0 + }; + return $.get(items, function(items) { + if (items.lastupdate > now - freq || items.lastchecked > now - $.DAY) { + return; } + return $.ajax('http://zixaphir.github.com/appchan-x/builds/version', { + onload: function() { + var el, version; + + if (this.status !== 200) { + return; + } + version = this.response; + if (!/^\d\.\d+\.\d+$/.test(version)) { + return; + } + if (g.VERSION === version) { + $.set('lastupdate', now); + return; + } + $.set('lastchecked', now); + el = $.el('span', { + innerHTML: "Update: appchan x v" + version + " is out, get it here." + }); + return new Notification('info', el, 120); + } + }); }); }, handleErrors: function(errors) { var div, error, logs, _i, _len; - if (!('length' in errors)) { + if (!(errors instanceof Array)) { error = errors; } else if (errors.length === 1) { error = errors[0]; @@ -12159,13 +12580,9 @@ innerHTML: "" + errors.length + " errors occurred. [show]" }); $.on(div.lastElementChild, 'click', function() { - if (this.textContent === 'show') { - this.textContent = 'hide'; - return logs.hidden = false; - } else { - this.textContent = 'show'; - return logs.hidden = true; - } + var _ref; + + return _ref = this.textContent === 'show' ? ['hide', false] : ['show', true], this.textContent = _ref[0], logs.hidden = _ref[1], _ref; }); logs = $.el('div', { hidden: true @@ -12179,17 +12596,40 @@ parseError: function(data) { var error, message; - message = data.message, error = data.error; - c.log(message, error); - c.log(message, error.stack); + Main.logError(data); message = $.el('div', { - textContent: message + textContent: data.message }); error = $.el('div', { - textContent: error + textContent: data.error }); return [message, error]; }, + errors: [], + logError: function(data) { + if (!Main.errors.length) { + $.on(window, 'unload', Main.postErrors); + } + c.error(data.message, data.error.stack); + return Main.errors.push(data); + }, + postErrors: function() { + var errors; + + errors = Main.errors.map(function(d) { + return d.message + ' ' + d.error.stack; + }); + return $.ajax('http://zixaphir.github.com/appchan-x/errors', {}, { + sync: true, + form: $.formData({ + n: "appchan x v" + g.VERSION, + t: 'userscript', + ua: window.navigator.userAgent, + url: window.location.href, + e: errors.join('\n') + }) + }); + }, isThisPageLegit: function() { var _ref; diff --git a/changelog-old b/changelog-old index efc097a00..1c0046608 100644 --- a/changelog-old +++ b/changelog-old @@ -9,6 +9,18 @@ master GIF thumbnail replacement, unlike Auto-GIF, actually works in /gif/ and /wsg/. Various little performance and readability tweaks. +2.39.3 +- Mayhem + Add /fa/ and /s4s/ archive redirection. + +2.39.2 +- Mayhem + Fix importing settings containing unicode characters. + +2.39.1 +- Mayhem + Add /gd/, /out/, /vp/ and /vr/ archive redirection. + 2.39.0 - Queue Fix rare bug in Relative Post Dates. diff --git a/css/style.css b/css/style.css index ae66b6610..f807975bc 100644 --- a/css/style.css +++ b/css/style.css @@ -14,7 +14,7 @@ margin: 0; padding: 2px 4px 3px; outline: none; - -webkit-transition: color .25s, border-color .25s, -webkit-flex .25s; + transition: color .25s, border-color .25s, -webkit-flex .25s; transition: color .25s, border-color .25s, flex .25s; } .field::-moz-placeholder, @@ -107,7 +107,6 @@ a[href="javascript:;"] { display: flex; padding: 3px 4px 4px; position: relative; - -webkit-transition: all .1s .05s ease-in-out; transition: all .1s .05s ease-in-out; } #board-list { @@ -120,7 +119,6 @@ a[href="javascript:;"] { margin-bottom: -1em; -webkit-transform: translateY(-100%); transform: translateY(-100%); - -webkit-transition: all .8s .6s cubic-bezier(.55, .055, .675, .19); transition: all .8s .6s cubic-bezier(.55, .055, .675, .19); } #toggle-header-bar { @@ -175,7 +173,6 @@ a[href="javascript:;"] { width: 500px; max-width: 100%; position: relative; - -webkit-transition: all .25s ease-in-out; transition: all .25s ease-in-out; } .notification.error { @@ -346,6 +343,9 @@ a[href="javascript:;"] { #updater:not(:hover) > div:not(.move) { display: none; } +#updater input[type="button"] { + width: 100%; +} .new { color: limegreen; } @@ -495,8 +495,9 @@ a[href="javascript:;"] { } /* QR */ -.hide-original-post-form #postForm, -.hide-original-post-form .postingMode, +:root.hide-original-post-form #postForm, +:root.hide-original-post-form .postingMode, +:root.hide-original-post-form #togglePostForm, #qr.autohide:not(:hover) > form { display: none; } @@ -545,11 +546,10 @@ a[href="javascript:;"] { flex: 1; } .persona .field:focus { - -webkit-flex: 4; - flex: 4; + -webkit-flex: 3; + flex: 3; } #dump-button { - background: -webkit-linear-gradient(#EEE, #CCC); background: linear-gradient(#EEE, #CCC); border: 1px solid #CCC; margin: 0; @@ -558,11 +558,9 @@ a[href="javascript:;"] { width: 30px; } #dump-button:hover, #dump-button:focus { - background: -webkit-linear-gradient(#FFF, #DDD); background: linear-gradient(#FFF, #DDD); } #dump-button:active, .dump #dump-button:not(:hover):not(:focus) { - background: -webkit-linear-gradient(#CCC, #DDD); background: linear-gradient(#CCC, #DDD); } .gecko #dump-button { @@ -614,7 +612,6 @@ a[href="javascript:;"] { overflow: hidden; position: relative; text-shadow: 0 1px 1px #000; - -webkit-transition: opacity .25s ease-in-out; transition: opacity .25s ease-in-out; vertical-align: top; white-space: pre; diff --git a/lib/$.coffee b/lib/$.coffee index 50b612144..01e5c5b1d 100644 --- a/lib/$.coffee +++ b/lib/$.coffee @@ -62,11 +62,6 @@ $.extend $, $.off d, 'DOMContentLoaded', cb fc() $.on d, 'DOMContentLoaded', cb - sync: (key, cb) -> - key = "#{g.NAMESPACE}#{key}" - $.on window, 'storage', (e) -> - if e.key is key - cb JSON.parse e.newValue formData: (form) -> if form instanceof HTMLFormElement return new FormData form @@ -81,15 +76,15 @@ $.extend $, fd.append key, val fd ajax: (url, callbacks, opts={}) -> - {type, headers, upCallbacks, form} = opts + {type, cred, headers, upCallbacks, form, sync} = opts r = new XMLHttpRequest() type or= form and 'post' or 'get' - r.open type, url, true + r.open type, url, !sync for key, val of headers r.setRequestHeader key, val $.extend r, callbacks $.extend r.upload, upCallbacks - r.withCredentials = type is 'post' + r.withCredentials = cred r.send form r cache: do -> @@ -101,12 +96,13 @@ $.extend $, else req.callbacks.push cb return + rm = -> delete reqs[url] req = $.ajax url, - onload: -> - cb.call @ for cb in @callbacks + onload: (e) -> + cb.call @, e for cb in @callbacks delete @callbacks - onabort: -> delete reqs[url] - onerror: -> delete reqs[url] + onabort: rm + onerror: rm req.callbacks = [cb] reqs[url] = req cb: @@ -244,19 +240,43 @@ $.extend $, # Round to an integer otherwise. Math.round size "#{size} #{['B', 'KB', 'MB', 'GB'][unit]}" - + syncing: {} + sync: do -> <% if (type === 'crx') { %> - delete: (keys) -> - chrome.storage.sync.remove keys - get: (key, defaultVal) -> - if val = localStorage.getItem g.NAMESPACE + key - JSON.parse val - else - defaultVal - set: (key, val) -> + chrome.storage.onChanged.addListener (changes) -> + for key of changes + if cb = $.syncing[key] + cb changes[key].newValue + return + (key, cb) -> $.syncing[key] = cb +<% } else { %> + window.addEventListener 'storage', (e) -> + if cb = $.syncing[e.key] + cb JSON.parse e.newValue + , false + (key, cb) -> $.syncing[g.NAMESPACE + key] = cb +<% } %> + item: (key, val) -> item = {} item[key] = val - chrome.storage.sync.set item + item +<% if (type === 'crx') { %> + # https://developer.chrome.com/extensions/storage.html + delete: (keys) -> + chrome.storage.sync.remove keys + get: (key, val, cb) -> + if typeof cb is 'function' + items = $.item key, val + else + items = key + cb = val + chrome.storage.sync.get items, cb + set: (key, val) -> + items = if typeof key is 'string' + $.item key, val + else + key + chrome.storage.sync.set items <% } else if (type === 'userjs') { %> do -> # http://www.opera.com/docs/userjs/specs/#scriptstorage @@ -275,19 +295,35 @@ do -> localStorage.removeItem key delete scriptStorage[key] return - $.get = (key, defaultVal) -> - if val = scriptStorage[g.NAMESPACE + key] - JSON.parse val + $.get = (key, val, cb) -> + if typeof cb is 'function' + items = $.item key, val else - defaultVal - $.set = (key, val) -> - key = g.NAMESPACE + key - val = JSON.stringify val - # for `storage` events - localStorage.setItem key, val - scriptStorage[key] = val + items = key + cb = val + $.queueTask -> + for key of items + if val = scriptStorage[g.NAMESPACE + key] + items[key] = JSON.parse val + cb items + $.set = do -> + set = (key, val) -> + key = g.NAMESPACE + key + val = JSON.stringify val + if key of $.syncing + # for `storage` events + localStorage.setItem key, val + scriptStorage[key] = val + (keys, val) -> + if typeof keys is 'string' + set keys, val + return + for key, val of keys + set key, val + return <% } else { %> - delete: (key) -> + # http://wiki.greasespot.net/Main_Page + delete: (keys) -> unless keys instanceof Array keys = [keys] for key in keys @@ -295,15 +331,30 @@ do -> localStorage.removeItem key GM_deleteValue key return - get: (key, defaultVal) -> - if val = GM_getValue g.NAMESPACE + key - JSON.parse val + get: (key, val, cb) -> + if typeof cb is 'function' + items = $.item key, val else - defaultVal - set: (key, val) -> - key = g.NAMESPACE + key - val = JSON.stringify val - # for `storage` events - localStorage.setItem key, val - GM_setValue key, val + items = key + cb = val + $.queueTask -> + for key of items + if val = GM_getValue g.NAMESPACE + key + items[key] = JSON.parse val + cb items + set: do -> + set = (key, val) -> + key = g.NAMESPACE + key + val = JSON.stringify val + if key of $.syncing + # for `storage` events + localStorage.setItem key, val + GM_setValue key, val + (keys, val) -> + if typeof keys is 'string' + set keys, val + return + for key, val of keys + set key, val + return <% } %> diff --git a/lib/ui.coffee b/lib/ui.coffee index c4fda1b35..14b8fe025 100644 --- a/lib/ui.coffee +++ b/lib/ui.coffee @@ -1,13 +1,12 @@ UI = do -> dialog = (id, position, html) -> - el = d.createElement 'div' - el.className = 'dialog' - el.innerHTML = html - el.id = id + el = $.el 'div', + className: 'dialog' + innerHTML: html + id: id el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position - move = el.querySelector '.move' - move.addEventListener 'touchstart', dragstart, false - move.addEventListener 'mousedown', dragstart, false + move = $ '.move', el + $.on move, 'touchstart mousedown', dragstart el @@ -103,8 +102,7 @@ UI = do -> $.rm currentMenu currentMenu = null lastToggledButton = null - $.off d, 'click', @close - $.off d, 'CloseMenu', @close + $.off d, 'click CloseMenu', @close findNextEntry: (entry, direction) -> entries = [entry.parentNode.children...] @@ -156,18 +154,14 @@ UI = do -> eRect = entry.getBoundingClientRect() cHeight = doc.clientHeight cWidth = doc.clientWidth - if eRect.top + sRect.height < cHeight - top = '0px' - bottom = 'auto' + [top, bottom] = if eRect.top + sRect.height < cHeight + ['0px', 'auto'] else - top = 'auto' - bottom = '0px' - if eRect.right + sRect.width < cWidth - left = '100%' - right = 'auto' + ['auto', '0px'] + [left, right] = if eRect.right + sRect.width < cWidth + ['100%', 'auto'] else - left = 'auto' - right = '100%' + ['auto', '100%'] {style} = submenu style.top = top style.bottom = bottom @@ -197,14 +191,13 @@ UI = do -> dragstart = (e) -> - if e.type is 'mousedown' and e.button isnt 0 # not LMB - return + return if e.type is 'mousedown' and e.button isnt 0 # not LMB # prevent text selection e.preventDefault() - el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @ if isTouching = e.type is 'touchstart' e = e.changedTouches[e.changedTouches.length - 1] # distance from pointer to el edge is constant; calculate it here. + el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @ rect = el.getBoundingClientRect() screenHeight = doc.clientHeight screenWidth = doc.clientWidth @@ -223,14 +216,13 @@ UI = do -> o.identifier = e.identifier o.move = touchmove.bind o o.up = touchend.bind o - d.addEventListener 'touchmove', o.move, false - d.addEventListener 'touchend', o.up, false - d.addEventListener 'touchcancel', o.up, false + $.on d, 'touchmove', o.move + $.on d, 'touchend touchcancel', o.up else # mousedown o.move = drag.bind o o.up = dragend.bind o - d.addEventListener 'mousemove', o.move, false - d.addEventListener 'mouseup', o.up, false + $.on d, 'mousemove', o.move + $.on d, 'mouseup', o.up touchmove = (e) -> for touch in e.changedTouches if touch.identifier is @identifier @@ -240,33 +232,29 @@ UI = do -> {clientX, clientY} = e left = clientX - @dx - left = - if left < 10 - 0 - else if @width - left < 10 - null - else - left / @screenWidth * 100 + '%' + left = if left < 10 + 0 + else if @width - left < 10 + null + else + left / @screenWidth * 100 + '%' top = clientY - @dy - top = - if top < 10 - 0 - else if @height - top < 10 - null - else - top / @screenHeight * 100 + '%' + top = if top < 10 + 0 + else if @height - top < 10 + null + else + top / @screenHeight * 100 + '%' - right = - if left is null - 0 - else - null - bottom = - if top is null - 0 - else - null + right = if left is null + 0 + else + null + bottom = if top is null + 0 + else + null {style} = @ style.left = left @@ -280,12 +268,11 @@ UI = do -> return dragend = -> if @isTouching - d.removeEventListener 'touchmove', @move, false - d.removeEventListener 'touchend', @up, false - d.removeEventListener 'touchcancel', @up, false + $.off d, 'touchmove', @move + $.off d, 'touchend touchcancel', @up else # mouseup - d.removeEventListener 'mousemove', @move, false - d.removeEventListener 'mouseup', @up, false + $.off d, 'mousemove', @move + $.off d, 'mouseup', @up localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) -> @@ -294,7 +281,7 @@ UI = do -> el: el style: el.style cb: cb - endEvents: endEvents.split ' ' + endEvents: endEvents latestEvent: latestEvent clientHeight: doc.clientHeight clientWidth: doc.clientWidth @@ -302,47 +289,39 @@ UI = do -> o.hover = hover.bind o o.hoverend = hoverend.bind o - asap = -> - if asapTest() - o.hover o.latestEvent - else - o.timeout = setTimeout asap, 25 - asap() + $.asap -> + !el.parentNode or asapTest() + , -> + o.hover o.latestEvent if el.parentNode - for event in o.endEvents - root.addEventListener event, o.hoverend, false - root.addEventListener 'mousemove', o.hover, false + $.on root, endEvents, o.hoverend + $.on root, 'mousemove', o.hover hover = (e) -> @latestEvent = e height = @el.offsetHeight {clientX, clientY} = e top = clientY - 120 - top = - if @clientHeight <= height or top <= 0 - 0 - else if top + height >= @clientHeight - @clientHeight - height - else - top - - if clientX <= @clientWidth - 400 - left = clientX + 45 + 'px' - right = null + top = if @clientHeight <= height or top <= 0 + 0 + else if top + height >= @clientHeight + @clientHeight - height else - left = null - right = @clientWidth - clientX + 45 + 'px' + top + + [left, right] = if clientX <= @clientWidth - 400 + [clientX + 45 + 'px', null] + else + [null, @clientWidth - clientX + 45 + 'px'] {style} = @ style.top = top + 'px' style.left = left style.right = right hoverend = -> - @el.parentNode.removeChild @el - for event in @endEvents - @root.removeEventListener event, @hoverend, false - @root.removeEventListener 'mousemove', @hover, false - clearTimeout @timeout + $.rm @el + $.off @root, @endEvents, @hoverend + $.off @root, 'mousemove', @hover @cb.call @ if @cb diff --git a/package.json b/package.json index 648d85b2d..90241bf31 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "grunt": "~0.4.1", "grunt-bump": "~0.0.0", "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-coffee": "~0.6.4", - "grunt-contrib-compress": "~0.4.5", + "grunt-contrib-coffee": "~0.6.5", + "grunt-contrib-compress": "~0.4.7", "grunt-contrib-concat": "~0.1.3", - "grunt-contrib-copy": "~0.4.0", + "grunt-contrib-copy": "~0.4.1", "grunt-contrib-watch": "~0.3.1", "grunt-exec": "~0.4.0" }, diff --git a/src/config.coffee b/src/config.coffee index d00549e01..03281f1af 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -49,6 +49,10 @@ Config = false 'Add buttons to navigate between threads.' ] + 'Reply Navigation': [ + false + 'Add buttons to navigate to top / bottom of thread.' + ] 'Check for Updates': [ true 'Check for updated versions of <%= meta.name %>.' @@ -232,7 +236,7 @@ Config = 'Add quote backlinks.' ] 'OP Backlinks': [ - false + true 'Add backlinks to the OP.' ] 'Quote Inlining': [ @@ -685,8 +689,8 @@ Config = MD5: '' sauces: """ -http://iqdb.org/?url=%TURL https://www.google.com/searchbyimage?image_url=%TURL +http://iqdb.org/?url=%TURL #//tineye.com/search?url=%TURL #http://saucenao.com/search.php?url=%TURL #http://3d.iqdb.org/?url=%TURL @@ -829,6 +833,7 @@ https://www.google.com/searchbyimage?image_url=%TURL 'x' 'Hide thread.' ] + updater: checkbox: 'Beep': [ diff --git a/src/databoard.coffee b/src/databoard.coffee new file mode 100644 index 000000000..b323d99ad --- /dev/null +++ b/src/databoard.coffee @@ -0,0 +1,88 @@ +DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts'] + +class DataBoard + constructor: (@key, sync) -> + @data = Conf[key] + $.sync key, @onSync.bind @ + @clean() + return unless sync + # Chrome also fires the onChanged callback on the current tab, + # so we only start syncing when we're ready. + $.on d, '4chanXInitFinished', => @sync = sync + + delete: ({boardID, threadID, postID}) -> + if postID + delete @data.boards[boardID][threadID][postID] + @deleteIfEmpty {boardID, threadID} + else if threadID + delete @data.boards[boardID][threadID] + @deleteIfEmpty {boardID} + else + delete @data.boards[boardID] + $.set @key, @data + + deleteIfEmpty: ({boardID, threadID}) -> + if threadID + unless Object.keys(@data.boards[boardID][threadID]).length + delete @data.boards[boardID][threadID] + @deleteIfEmpty {boardID} + else unless Object.keys(@data.boards[boardID]).length + delete @data.boards[boardID] + + set: ({boardID, threadID, postID, val}) -> + if postID + ((@data.boards[boardID] or= {})[threadID] or= {})[postID] = val + else if threadID + (@data.boards[boardID] or= {})[threadID] = val + else + @data.boards[boardID] = val + $.set @key, @data + + get: ({boardID, threadID, postID, defaultValue}) -> + if board = @data.boards[boardID] + unless threadID + if postID + for ID, thread in board + if postID of thread + val = thread[postID] + break + else + val = board + else if thread = board[threadID] + val = if postID + thread[postID] + else + thread + val or defaultValue + + clean: -> + for boardID of @data.boards + @deleteIfEmpty {boardID} + + now = Date.now() + if (@data.lastChecked or 0) < now - 12 * $.HOUR + @data.lastChecked = now + for boardID of @data.boards + @ajaxClean boardID + + $.set @key, @data + + ajaxClean: (boardID) -> + $.cache "//api.4chan.org/#{boardID}/threads.json", (e) => + if e.target.status is 404 + # Deleted board. + @delete boardID + else if e.target.status is 200 + board = @data.boards[boardID] + threads = {} + for page in JSON.parse e.target.response + for thread in page.threads + if thread.no of board + threads[thread.no] = board[thread.no] + @data.boards[boardID] = threads + @deleteIfEmpty {boardID} + $.set @key, @data + + onSync: (data) -> + @data = data or boards: {} + @sync?() diff --git a/src/features.coffee b/src/features.coffee index 8ae49caf2..acd3c183f 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -44,10 +44,10 @@ Header = nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) -> if /^[^\w@]/.test t return $.tn t - if t is 'toggle-all' + if /^toggle-all/.test t a = $.el 'a', className: 'show-board-list-button' - textContent: '+' + textContent: (t.match(/-text:"(.+)"/) || [null, '+'])[1] href: 'javascript:;' $.on a, 'click', Header.toggleBoardList return a @@ -191,10 +191,34 @@ Settings = $.asap (-> $.id 'boardNavMobile'), -> $.prepend $.id('navtopright'), [$.tn(' ['), link, $.tn('] ')] - if (prevVersion = $.get 'previousversion', null) isnt g.VERSION - $.set 'lastupdate', Date.now() - $.set 'previousversion', g.VERSION - $.on d, '4chanXInitFinished', Settings.open unless prevVersion + $.get 'previousversion', null, (item) -> + <% if (type === 'userscript') { %> + el = $.el 'span' + el.style.flex = 'test' + if el.style.flex is 'test' + el.innerHTML = """ + Firefox is not correctly set up and some <%= meta.name %> features will be displayed incorrectly.
+ Follow the instructions of the install guide to fix it. + """ + new Notification 'warning', el, 30 + <% } %> + if previous = item['previousversion'] + return if previous is g.VERSION + # Avoid conflicts between sync'd newer versions + # and out of date extension on this device. + prev = previous.match(/\d+/g).map Number + curr = g.VERSION.match(/\d+/g).map Number + return unless prev[0] <= curr[0] and prev[1] <= curr[1] and prev[2] <= curr[2] + + changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' + el = $.el 'span', + innerHTML: "<%= meta.name %> has been updated to version #{g.VERSION}." + new Notification 'info', el, 30 + else + $.on d, '4chanXInitFinished', Settings.open + $.set + lastupdate: Date.now() + previousversion: g.VERSION Settings.addSection 'Main', Settings.main Settings.addSection 'Filter', Settings.filter @@ -220,7 +244,7 @@ Settings =
@@ -290,37 +314,63 @@ Settings = $.on $('.import', section), 'click', Settings.import $.on $('input', section), 'change', Settings.onImport + items = {} + inputs = {} for key, obj of Config.main fs = $.el 'fieldset', innerHTML: "#{key}" for key, arr of obj - checked = if $.get(key, Conf[key]) then 'checked' else '' description = arr[1] div = $.el 'div', - innerHTML: ": #{description}" - $.on $('input', div), 'change', $.cb.checked + innerHTML: ": #{description}" + input = $ 'input', div + $.on input, 'change', $.cb.checked + items[key] = Conf[key] + inputs[key] = input $.add fs, div $.add section, fs - hiddenNum = 0 - for ID, thread of ThreadHiding.getHiddenThreads().threads - hiddenNum++ - for ID, thread of ReplyHiding.getHiddenPosts().threads - for ID, post of thread - hiddenNum++ + $.get items, (items) -> + for key, val of items + inputs[key].checked = val + return + div = $.el 'div', - innerHTML: ": Clear manually hidden threads and posts on /#{g.BOARD}/." - $.on $('button', div), 'click', -> + innerHTML: ": Clear manually-hidden threads and posts on all boards. Refresh the page to apply." + button = $ 'button', div + hiddenNum = 0 + $.get 'hiddenThreads', boards: {}, (item) -> + for ID, board of item.hiddenThreads.boards + for ID, thread of board + hiddenNum++ + button.textContent = "Hidden: #{hiddenNum}" + $.get 'hiddenPosts', boards: {}, (item) -> + for ID, board of item.hiddenPosts.boards + for ID, thread of board + for ID, post of thread + hiddenNum++ + button.textContent = "Hidden: #{hiddenNum}" + $.on button, 'click', -> @textContent = 'Hidden: 0' - $.delete ["hiddenThreads.#{g.BOARD}", "hiddenPosts.#{g.BOARD}"] + $.get 'hiddenThreads', boards: {}, (item) -> + for boardID of item.hiddenThreads.boards + localStorage.removeItem "4chan-hide-t-#{boardID}" + $.delete ['hiddenThreads', 'hiddenPosts'] $.after $('input[name="Stubs"]', section).parentNode.parentNode, div - export: -> - now = Date.now() - data = - version: g.VERSION - date: now - Conf: Conf - WatchedThreads: $.get('WatchedThreads', {}) + export: (now, data) -> + unless typeof now is 'number' + now = Date.now() + data = + version: g.VERSION + date: now + Conf['WatchedThreads'] = {} + for db in DataBoards + Conf[db] = boards: {} + # Make sure to export the most recent data. + $.get Conf, (Conf) -> + data.Conf = Conf + Settings.export now, data + return a = $.el 'a', className: 'warning' textContent: 'Save me!' @@ -331,9 +381,9 @@ Settings = a.click() return # XXX Firefox won't let us download automatically. - output = @parentNode.nextElementSibling - output.innerHTML = null - $.add output, a + p = $ '.imp-exp-result', Settings.dialog + p.innerHTML = null + $.add p, a import: -> @nextElementSibling.click() onImport: -> @@ -345,13 +395,13 @@ Settings = reader = new FileReader() reader.onload = (e) -> try - data = JSON.parse decodeURIComponent escape e.target.result + data = JSON.parse e.target.result Settings.loadSettings data if confirm 'Import successful. Refresh now?' window.location.reload() catch err output.textContent = 'Import failed due to an error.' - c.log err.stack + c.error err.stack reader.readAsText file loadSettings: (data) -> version = data.version.split '.' @@ -421,9 +471,8 @@ Settings = continue unless key of data.Conf data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) -> "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}" - for key, val of data.Conf - $.set key, val - $.set 'WatchedThreads', data.WatchedThreads + data.Conf.WatchedThreads = data.WatchedThreads + $.set data.Conf convertSettings: (data, map) -> for prevKey, newKey of map data.Conf[newKey] = data.Conf[prevKey] if newKey @@ -459,8 +508,9 @@ Settings = ta = $.el 'textarea', name: name className: 'field' - value: $.get name, Conf[name] spellcheck: false + $.get name, Conf[name], (item) -> + ta.value = item[name] $.on ta, 'change', $.cb.value $.add div, ta return @@ -510,7 +560,8 @@ Settings = """ sauce = $ 'textarea', section - sauce.value = $.get 'sauces', Conf['sauces'] + $.get 'sauces', Conf['sauces'], (item) -> + sauce.value = item['sauces'] $.on sauce, 'change', $.cb.value rice: (section) -> @@ -573,17 +624,25 @@ Settings = """ + items = {} + inputs = {} for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss'] input = $ "[name=#{name}]", section - input.value = $.get name, Conf[name] + items[name] = Conf[name] + inputs[name] = input event = if ['favicon', 'usercss'].contains name 'change' else 'input' $.on input, event, $.cb.value - unless 'usercss' is name - $.on input, event, Settings[name] - Settings[name].call input + $.get items, (items) -> + for key, val of items + input = inputs[key] + input.value = val + unless 'usercss' is name + $.on input, event, Settings[key] + Settings[key].call input + return $.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss $.on $.id('apply-css'), 'click', Settings.usercss boardnav: -> @@ -633,17 +692,23 @@ Settings = ActionsKeybinds """ - tbody = $ 'tbody', section + tbody = $ 'tbody', section + items = {} + inputs = {} for key, arr of Config.hotkeys tr = $.el 'tr', innerHTML: "#{arr[1]}" input = $ 'input', tr - input.name = key - input.value = $.get key, Conf[key] + input.name = key input.spellcheck = false + items[key] = Conf[key] + inputs[key] = input $.on input, 'keydown', Settings.keybind $.add tbody, tr - return + $.get items, (items) -> + for key, val of items + inputs[key].value = val + return keybind: (e) -> return if e.keyCode is 9 # tab e.preventDefault() @@ -818,7 +883,7 @@ Filter = # Hide if result.hide if @isReply - ReplyHiding.hide @, result.stub + PostHiding.hide @, result.stub else if g.VIEW is 'index' ThreadHiding.hide @thread, result.stub else @@ -971,13 +1036,14 @@ Filter = re += ';op:yes' # Add a new line before the regexp unless the text is empty. - save = $.get type, '' - save = - if save - "#{save}\n#{re}" - else - re - $.set type, save + $.get type, '', (item) -> + save = item[type] + save = + if save + "#{save}\n#{re}" + else + re + $.set type, save # Open the settings and display & focus the relevant filter textarea. Settings.open 'Filter' @@ -994,41 +1060,63 @@ ThreadHiding = init: -> return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link'] - Misc.clearThreads "hiddenThreads.#{g.BOARD}" - @getHiddenThreads() - @syncFromCatalog() + @db = new DataBoard 'hiddenThreads' + @syncCatalog() Thread::callbacks.push name: 'Thread Hiding' cb: @node node: -> - if data = ThreadHiding.hiddenThreads.threads[@] + if data = ThreadHiding.db.get {boardID: @board.ID, threadID: @ID} ThreadHiding.hide @, data.makeStub return unless Conf['Thread Hiding'] $.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide' - getHiddenThreads: -> - ThreadHiding.hiddenThreads = $.get "hiddenThreads.#{g.BOARD}", threads: {} - - syncFromCatalog: -> + syncCatalog: -> # Sync hidden threads from the catalog into the index. - hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} - {threads} = ThreadHiding.hiddenThreads + hiddenThreads = ThreadHiding.db.get + boardID: g.BOARD.ID + defaultValue: {} + # XXX tmp fix + try + hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} + catch e + localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify {} + return ThreadHiding.syncCatalog() # Add threads that were hidden in the catalog. for threadID of hiddenThreadsOnCatalog - continue if threadID of threads - threads[threadID] = {} + unless threadID of hiddenThreads + hiddenThreads[threadID] = {} # Remove threads that were un-hidden in the catalog. - for threadID of threads - continue if threadID of threads - delete threads[threadID] + for threadID of hiddenThreads + unless threadID of hiddenThreadsOnCatalog + delete hiddenThreads[threadID] - if Object.keys(threads).length - $.set "hiddenThreads.#{g.BOARD}", ThreadHiding.hiddenThreads - else - $.delete "hiddenThreads.#{g.BOARD}" + if (ThreadHiding.db.data.lastChecked or 0) > Date.now() - $.MINUTE + # Was cleaned just now. + ThreadHiding.cleanCatalog hiddenThreadsOnCatalog + + ThreadHiding.db.set + boardID: g.BOARD.ID + val: hiddenThreads + + cleanCatalog: (hiddenThreadsOnCatalog) -> + # We need to clean hidden threads on the catalog ourselves, + # otherwise if we don't visit the catalog regularly + # it will pollute the localStorage and our data. + $.cache "//api.4chan.org/#{g.BOARD}/threads.json", -> + return unless @status is 200 + threads = {} + for page in JSON.parse @response + for thread in page.threads + if thread.no of hiddenThreadsOnCatalog + threads[thread.no] = hiddenThreadsOnCatalog[thread.no] + if Object.keys(threads).length + localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify threads + else + localStorage.removeItem "4chan-hide-t-#{g.BOARD}" menu: init: -> @@ -1073,17 +1161,19 @@ ThreadHiding = a saveHiddenState: (thread, makeStub) -> - # Get fresh hidden threads. - hiddenThreads = ThreadHiding.getHiddenThreads() - hiddenThreadsCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} + hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} if thread.isHidden - hiddenThreads.threads[thread] = {makeStub} - hiddenThreadsCatalog[thread] = true + ThreadHiding.db.set + boardID: thread.board.ID + threadID: thread.ID + val: {makeStub} + hiddenThreadsOnCatalog[thread] = true else - delete hiddenThreads.threads[thread] - delete hiddenThreadsCatalog[thread] - $.set "hiddenThreads.#{g.BOARD}", hiddenThreads - localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsCatalog + ThreadHiding.db.delete + boardID: thread.board.ID + threadID: thread.ID + delete hiddenThreadsOnCatalog[thread] + localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsOnCatalog toggle: (thread) -> unless thread instanceof Thread @@ -1132,31 +1222,26 @@ ThreadHiding = threadRoot.nextElementSibling.hidden = threadRoot.hidden = thread.isHidden = false -ReplyHiding = +PostHiding = init: -> return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] - Misc.clearThreads "hiddenPosts.#{g.BOARD}" - @getHiddenPosts() + @db = new DataBoard 'hiddenPosts' Post::callbacks.push name: 'Reply Hiding' cb: @node node: -> return if !@isReply or @isClone - if thread = ReplyHiding.hiddenPosts.threads[@thread] - if data = thread[@] - if data.thisPost - ReplyHiding.hide @, data.makeStub, data.hideRecursively - else - Recursive.apply ReplyHiding.hide, @, data.makeStub, true - Recursive.add ReplyHiding.hide, @, data.makeStub, true + if data = PostHiding.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID} + if data.thisPost + PostHiding.hide @, data.makeStub, data.hideRecursively + else + Recursive.apply PostHiding.hide, @, data.makeStub, true + Recursive.add PostHiding.hide, @, data.makeStub, true return unless Conf['Reply Hiding'] $.add $('.postInfo', @nodes.post), ReplyHiding.makeButton @, 'hide' - getHiddenPosts: -> - ReplyHiding.hiddenPosts = $.get "hiddenPosts.#{g.BOARD}", threads: {} - menu: init: -> return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding Link'] @@ -1169,7 +1254,7 @@ ReplyHiding = apply = $.el 'a', textContent: 'Apply' href: 'javascript:;' - $.on apply, 'click', ReplyHiding.menu.hide + $.on apply, 'click', PostHiding.menu.hide thisPost = $.el 'label', innerHTML: ' This post' @@ -1185,7 +1270,7 @@ ReplyHiding = open: (post) -> if !post.isReply or post.isClone or post.isHidden return false - ReplyHiding.menu.post = post + PostHiding.menu.post = post true subEntries: [{el: apply}, {el: thisPost}, {el: replies}, {el: makeStub}] @@ -1197,7 +1282,7 @@ ReplyHiding = apply = $.el 'a', textContent: 'Apply' href: 'javascript:;' - $.on apply, 'click', ReplyHiding.menu.show + $.on apply, 'click', PostHiding.menu.show thisPost = $.el 'label', innerHTML: ' This post' @@ -1209,47 +1294,45 @@ ReplyHiding = el: div order: 20 open: (post) -> - if !post.isReply or post.isClone + if !post.isReply or post.isClone or !post.isHidden return false - thread = ReplyHiding.getHiddenPosts().threads[post.thread] - unless post.isHidden or data = thread?[post] + unless data = PostHiding.db.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID} return false - ReplyHiding.menu.post = post + PostHiding.menu.post = post thisPost.firstChild.checked = post.isHidden replies.firstChild.checked = if data?.hideRecursively? then data.hideRecursively else Conf['Recursive Hiding'] true subEntries: [{el: apply}, {el: thisPost}, {el: replies}] + hide: -> parent = @parentNode thisPost = $('input[name=thisPost]', parent).checked replies = $('input[name=replies]', parent).checked makeStub = $('input[name=makeStub]', parent).checked - {post} = ReplyHiding.menu + {post} = PostHiding.menu if thisPost - ReplyHiding.hide post, makeStub, replies + PostHiding.hide post, makeStub, replies else if replies - Recursive.apply ReplyHiding.hide, post, makeStub, true - Recursive.add ReplyHiding.hide, post, makeStub, true + Recursive.apply PostHiding.hide, post, makeStub, true + Recursive.add PostHiding.hide, post, makeStub, true else return - ReplyHiding.saveHiddenState post, true, thisPost, makeStub, replies + PostHiding.saveHiddenState post, true, thisPost, makeStub, replies $.event 'CloseMenu' show: -> parent = @parentNode thisPost = $('input[name=thisPost]', parent).checked replies = $('input[name=replies]', parent).checked - {post} = ReplyHiding.menu - thread = ReplyHiding.getHiddenPosts().threads[post.thread] - data = thread?[post] + {post} = PostHiding.menu if thisPost - ReplyHiding.show post, replies + PostHiding.show post, replies else if replies - Recursive.apply ReplyHiding.show, post, true - Recursive.rm ReplyHiding.hide, post, true + Recursive.apply PostHiding.show, post, true + Recursive.rm PostHiding.hide, post, true else return - if data - ReplyHiding.saveHiddenState post, !(thisPost and replies), !thisPost, data.makeStub, !replies + if data = PostHiding.db.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID} + PostHiding.saveHiddenState post, !(thisPost and replies), !thisPost, data.makeStub, !replies $.event 'CloseMenu' makeButton: (post, type) -> @@ -1257,41 +1340,38 @@ ReplyHiding = className: "#{type}-reply-button" innerHTML: "[ #{if type is 'hide' then '-' else '+'} ]" href: 'javascript:;' - $.on a, 'click', ReplyHiding.toggle + $.on a, 'click', PostHiding.toggle a saveHiddenState: (post, isHiding, thisPost, makeStub, hideRecursively) -> - # Get fresh hidden posts. - hiddenPosts = ReplyHiding.getHiddenPosts() + data = + boardID: post.board.ID + threadID: post.thread.ID + postID: post.ID if isHiding - unless thread = hiddenPosts.threads[post.thread] - thread = hiddenPosts.threads[post.thread] = {} - thread[post] = + data.val = thisPost: thisPost isnt false # undefined -> true makeStub: makeStub hideRecursively: hideRecursively + PostHiding.db.set data else - thread = hiddenPosts.threads[post.thread] - delete thread[post] - unless Object.keys(thread).length - delete hiddenPosts.threads[post.thread] - $.set "hiddenPosts.#{g.BOARD}", hiddenPosts + PostHiding.db.delete data toggle: -> post = Get.postFromNode @ if post.isHidden - ReplyHiding.show post + PostHiding.show post else - ReplyHiding.hide post - ReplyHiding.saveHiddenState post, post.isHidden + PostHiding.hide post + PostHiding.saveHiddenState post, post.isHidden hide: (post, makeStub=Conf['Stubs'], hideRecursively=Conf['Recursive Hiding']) -> return if post.isHidden post.isHidden = true if hideRecursively - Recursive.apply ReplyHiding.hide, post, makeStub, true - Recursive.add ReplyHiding.hide, post, makeStub, true + Recursive.apply PostHiding.hide, post, makeStub, true + Recursive.add PostHiding.hide, post, makeStub, true for quotelink in Get.allQuotelinksLinkingTo post $.addClass quotelink, 'filtered' @@ -1300,7 +1380,7 @@ ReplyHiding = post.nodes.root.hidden = true return - a = ReplyHiding.makeButton post, 'show' + a = PostHiding.makeButton post, 'show' postInfo = if Conf['Anonymize'] 'Anonymous' @@ -1322,8 +1402,8 @@ ReplyHiding = post.nodes.root.hidden = false post.isHidden = false if showRecursively - Recursive.apply ReplyHiding.show, post, true - Recursive.rm ReplyHiding.hide, post + Recursive.apply PostHiding.show, post, true + Recursive.rm PostHiding.hide, post for quotelink in Get.allQuotelinksLinkingTo post $.rmClass quotelink, 'filtered' return @@ -1378,8 +1458,8 @@ QuoteStrikeThrough = node: -> return if @isClone for quotelink in @nodes.quotelinks - {board, postID} = Get.postDataFromLink quotelink - if g.posts["#{board}.#{postID}"]?.isHidden + {boardID, postID} = Get.postDataFromLink quotelink + if g.posts["#{boardID}.#{postID}"]?.isHidden $.addClass quotelink, 'filtered' return @@ -1476,22 +1556,17 @@ DeleteLink = el: div order: 40 open: (post) -> - return false if post.isDead or !((thread = QR.yourPosts.threads[post.thread]) and thread.contains post.ID) + return false if post.isDead DeleteLink.post = post - DeleteLink.cooldown.start post node = div.firstChild - if seconds = DeleteLink.cooldown[post.fullID] - node.textContent = "Delete (#{seconds})" - DeleteLink.cooldown.el = node - else - node.textContent = 'Delete' - delete DeleteLink.cooldown.el + node.textContent = 'Delete' + DeleteLink.cooldown.start post, node true subEntries: [postEntry, fileEntry] delete: -> {post} = DeleteLink - return if DeleteLink.cooldown[post.fullID] + return if DeleteLink.cooldown.counting is post $.off @, 'click', DeleteLink.delete @textContent = "Deleting #{@textContent}..." @@ -1509,13 +1584,13 @@ DeleteLink = form[post.ID] = 'delete' link = @ - $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), { - onload: -> DeleteLink.load link, @response - onerror: -> DeleteLink.error link - }, { - form: $.formData form - } - load: (link, html) -> + $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), + onload: -> DeleteLink.load link, post, @response + onerror: -> DeleteLink.error link + , + cred: true + form: $.formData form + load: (link, post, html) -> tmpDoc = d.implementation.createHTMLDocument '' tmpDoc.documentElement.innerHTML = html if tmpDoc.title is '4chan - Banned' # Ban/warn check @@ -1524,6 +1599,9 @@ DeleteLink = s = msg.textContent $.on link, 'click', DeleteLink.delete else + if tmpDoc.title is 'Updating index...' + # We're 100% sure. + (post.origin or post).kill() s = 'Deleted' link.textContent = s error: (link) -> @@ -1531,25 +1609,29 @@ DeleteLink = $.on link, 'click', DeleteLink.delete cooldown: - start: (post) -> - return if post.fullID of DeleteLink.cooldown + start: (post, node) -> + unless QR.db?.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID} + # Only start counting on our posts. + delete DeleteLink.cooldown.counting + return + DeleteLink.cooldown.counting = post length = if post.board.ID is 'q' 600 else 30 seconds = Math.ceil (length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND - DeleteLink.cooldown.count post.fullID, seconds, length - count: (fullID, seconds, length) -> - return unless 0 <= seconds <= length - setTimeout DeleteLink.cooldown.count, 1000, fullID, seconds-1, length - {el} = DeleteLink.cooldown - if seconds is 0 - el?.textContent = 'Delete' - delete DeleteLink.cooldown[fullID] - delete DeleteLink.cooldown.el + DeleteLink.cooldown.count post, seconds, length, node + count: (post, seconds, length, node) -> + return if DeleteLink.cooldown.counting isnt post + unless 0 <= seconds <= length + if DeleteLink.cooldown.counting is post + delete DeleteLink.cooldown.counting return - el?.textContent = "Delete (#{seconds})" - DeleteLink.cooldown[fullID] = seconds + setTimeout DeleteLink.cooldown.count, 1000, post, seconds - 1, length, node + if seconds is 0 + node.textContent = 'Delete' + return + node.textContent = "Delete (#{seconds})" DownloadLink = init: -> @@ -1582,8 +1664,8 @@ ArchiveLink = type: 'post' el: div order: 90 - open: ({ID: postID, thread: threadID, board}) -> - redirect = Redirect.to {postID, threadID, board} + open: ({ID, thread, board}) -> + redirect = Redirect.to {postID: ID, threadID: thread.ID, boardID: board.ID} redirect isnt "//boards.4chan.org/#{board}/" subEntries: [] @@ -1606,17 +1688,17 @@ ArchiveLink = textContent: text target: '_blank' - if type is 'post' - open = ({ID: postID, thread: threadID, board}) -> - el.href = Redirect.to {postID, threadID, board} + open = if type is 'post' + ({ID, thread, board}) -> + el.href = Redirect.to {postID: ID, threadID: thread.ID, boardID: board.ID} true else - open = (post) -> + (post) -> value = Filter[type] post # We want to parse the exact same stuff as the filter does already. return false unless value el.href = Redirect.to - board: post.board + boardID: post.board.ID type: type value: value isSearch: true @@ -1827,7 +1909,7 @@ Keybinds = Nav = init: -> - return if g.VIEW isnt 'index' or !Conf['Index Navigation'] + return if g.VIEW is 'index' and !Conf['Index Navigation'] or g.VIEW is 'thread' and !Conf['Reply Navigation'] span = $.el 'span', id: 'navlinks' @@ -1845,10 +1927,16 @@ Nav = $.on d, '4chanXInitFinished', -> $.add d.body, span prev: -> - Nav.scroll -1 + if g.VIEW is 'thread' + window.scrollTo 0, 0 + else + Nav.scroll -1 next: -> - Nav.scroll +1 + if g.VIEW is 'thread' + window.scrollTo 0, d.body.scrollHeight + else + Nav.scroll +1 getThread: (full) -> headRect = Header.bar.getBoundingClientRect() @@ -1874,90 +1962,84 @@ Nav = window.scrollBy 0, top Redirect = - image: (board, filename) -> + image: (boardID, filename) -> # Do not use g.BOARD, the image url can originate from a cross-quote. - switch "#{board}" - when 'a', 'jp', 'm', 'q', 'tg', 'vg', 'wsg' - "//archive.foolz.us/#{board}/full_image/#{filename}" + switch boardID + when 'a', 'gd', 'jp', 'm', 'q', 'tg', 'vg', 'vp', 'vr', 'wsg' + "//archive.foolz.us/#{boardID}/full_image/#{filename}" when 'u' - "//nsfw.foolz.us/#{board}/full_image/#{filename}" + "//nsfw.foolz.us/#{boardID}/full_image/#{filename}" when 'po' - "//archive.thedarkcave.org/#{board}/full_image/#{filename}" - when 'ck', 'lit' - "//fuuka.warosu.org/#{board}/full_image/#{filename}" + "//archive.thedarkcave.org/#{boardID}/full_image/#{filename}" + when 'ck', 'fa', 'lit', 's4s' + "//fuuka.warosu.org/#{boardID}/full_image/#{filename}" when 'cgl', 'g', 'mu', 'w' - "//rbt.asia/#{board}/full_image/#{filename}" + "//rbt.asia/#{boardID}/full_image/#{filename}" when 'an', 'k', 'toy', 'x' - "http://archive.heinessen.com/#{board}/full_image/#{filename}" + "http://archive.heinessen.com/#{boardID}/full_image/#{filename}" when 'c' - "//archive.nyafuu.org/#{board}/full_image/#{filename}" - post: (board, postID) -> - switch "#{board}" - when 'a', 'co', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'wsg' - "//archive.foolz.us/_/api/chan/post/?board=#{board}&num=#{postID}" + "//archive.nyafuu.org/#{boardID}/full_image/#{filename}" + post: (boardID, postID) -> + switch boardID + when 'a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg' + "//archive.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}" when 'u' - "//nsfw.foolz.us/_/api/chan/post/?board=#{board}&num=#{postID}" - when 'c', 'int', 'po' - "//archive.thedarkcave.org/_/api/chan/post/?board=#{board}&num=#{postID}" + "//nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}" + when 'c', 'int', 'out', 'po' + "//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}" # for fuuka-based archives: # https://github.com/eksopl/fuuka/issues/27 to: (data) -> - {board} = data - switch "#{board}" - when 'a', 'co', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'wsg' - url = Redirect.path '//archive.foolz.us', 'foolfuuka', data + {boardID} = data + switch boardID + when 'a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg' + Redirect.path '//archive.foolz.us', 'foolfuuka', data when 'u' - url = Redirect.path '//nsfw.foolz.us', 'foolfuuka', data - when 'int', 'po' - url = Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data - when 'ck', 'lit' - url = Redirect.path '//fuuka.warosu.org', 'fuuka', data + Redirect.path '//nsfw.foolz.us', 'foolfuuka', data + when 'int', 'out', 'po' + Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data + when 'ck', 'fa', 'lit', 's4s' + Redirect.path '//fuuka.warosu.org', 'fuuka', data when 'diy', 'sci' - url = Redirect.path '//archive.installgentoo.net', 'fuuka', data + Redirect.path '//archive.installgentoo.net', 'fuuka', data when 'cgl', 'g', 'mu', 'w' - url = Redirect.path '//rbt.asia', 'fuuka', data + Redirect.path '//rbt.asia', 'fuuka', data when 'an', 'fit', 'k', 'mlp', 'r9k', 'toy', 'x' - url = Redirect.path 'http://archive.heinessen.com', 'fuuka', data + Redirect.path 'http://archive.heinessen.com', 'fuuka', data when 'c' - url = Redirect.path '//archive.nyafuu.org', 'fuuka', data + Redirect.path '//archive.nyafuu.org', 'fuuka', data else - if data.threadID - url = "//boards.4chan.org/#{board}/" - url or '' + if data.threadID then "//boards.4chan.org/#{boardID}/" else '' path: (base, archiver, data) -> if data.isSearch - {board, type, value} = data - type = - if type is 'name' - 'username' - else if type is 'MD5' - 'image' - else - type + {boardID, type, value} = data + type = if type is 'name' + 'username' + else if type is 'MD5' + 'image' + else + type value = encodeURIComponent value return if archiver is 'foolfuuka' - "#{base}/#{board}/search/#{type}/#{value}" - else if type is 'image' - "#{base}/#{board}/?task=search2&search_media_hash=#{value}" - else - "#{base}/#{board}/?task=search2&search_#{type}=#{value}" - - {board, threadID, postID} = data - # keep the number only if the location.hash was sent f.e. - postID = postID.match(/\d+/)[0] if postID and typeof postID is 'string' - path = - if threadID - "#{board}/thread/#{threadID}" + "#{base}/#{boardID}/search/#{type}/#{value}" + else if type is 'image' + "#{base}/#{boardID}/?task=search2&search_media_hash=#{value}" else - "#{board}/post/#{postID}" + "#{base}/#{boardID}/?task=search2&search_#{type}=#{value}" + + {boardID, threadID, postID} = data + # keep the number only if the location.hash was sent f.e. + path = if threadID + "#{boardID}/thread/#{threadID}" + else + "#{boardID}/post/#{postID}" if archiver is 'foolfuuka' path += '/' if threadID and postID - path += - if archiver is 'foolfuuka' - "##{postID}" - else - "#p#{postID}" + path += if archiver is 'foolfuuka' + "##{postID}" + else + "#p#{postID}" "#{base}/#{path}" Build = @@ -1971,12 +2053,12 @@ Build = "#{filename[...threshold - 5]}(...).#{filename[-3..]}" else filename - postFromObject: (data, board) -> + postFromObject: (data, boardID) -> o = # id postID: data.no threadID: data.resto or data.no - board: board + boardID: boardID # info name: data.name capcode: data.capcode @@ -1997,12 +2079,12 @@ Build = o.file = name: data.filename + data.ext timestamp: "#{data.tim}#{data.ext}" - url: "//images.4chan.org/#{board}/src/#{data.tim}#{data.ext}" + url: "//images.4chan.org/#{boardID}/src/#{data.tim}#{data.ext}" height: data.h width: data.w MD5: data.md5 size: data.fsize - turl: "//thumbs.4chan.org/#{board}/thumb/#{data.tim}s.jpg" + turl: "//thumbs.4chan.org/#{boardID}/thumb/#{data.tim}s.jpg" theight: data.tn_h twidth: data.tn_w isSpoiler: !!data.spoiler @@ -2014,7 +2096,7 @@ Build = @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE ### { - postID, threadID, board + postID, threadID, boardID name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC isSticky, isClosed comment @@ -2069,7 +2151,7 @@ Build = flag = if flagCode - " #{flagCode}" else '' @@ -2097,13 +2179,13 @@ Build = fileSize = "Spoiler Image, #{fileSize}" unless isArchived fileThumb = '//static.4chan.org/image/spoiler' - if spoilerRange = Build.spoilerRange[board] + if spoilerRange = Build.spoilerRange[boardID] # Randomize the spoiler image. - fileThumb += "-#{board}" + Math.floor 1 + spoilerRange * Math.random() + fileThumb += "-#{boardID}" + Math.floor 1 + spoilerRange * Math.random() fileThumb += '.png' file.twidth = file.theight = 100 - if board.ID isnt 'f' + if boardID.ID isnt 'f' imgSrc = "" + "#{fileSize}" @@ -2168,12 +2250,12 @@ Build = capcodeStart + capcode + userID + flag + sticky + closed + "
#{subject}" + "
#{date}" + - "No." + + "No." + "#{postID}" + '' + '' + @@ -2190,12 +2272,12 @@ Build = '
' + "#{date} " + "" + - "No." + + "No." + "#{postID}" + '' + '' + @@ -2209,7 +2291,7 @@ Build = for quote in $$ '.quotelink', container href = quote.getAttribute 'href' continue if href[0] is '/' # Cross-board quote, or board link - quote.href = "/#{board}/res/#{href}" # Fix pathnames + quote.href = "/#{boardID}/res/#{href}" # Fix pathnames container @@ -2222,11 +2304,11 @@ Get = $('.nameBlock', OP.nodes.info).textContent.trim() "/#{thread.board}/ - #{excerpt}" postFromRoot: (root) -> - link = $ 'a[title="Highlight this post"]', root - board = link.pathname.split('/')[1] - postID = link.hash[2..] - index = root.dataset.clone - post = g.posts["#{board}.#{postID}"] + link = $ 'a[title="Highlight this post"]', root + boardID = link.pathname.split('/')[1] + postID = link.hash[2..] + index = root.dataset.clone + post = g.posts["#{boardID}.#{postID}"] if index then post.clones[index] else post postFromNode: (root) -> Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', root @@ -2235,15 +2317,15 @@ Get = postDataFromLink: (link) -> if link.hostname is 'boards.4chan.org' path = link.pathname.split '/' - board = path[1] + boardID = path[1] threadID = path[3] postID = link.hash[2..] else # resurrected quote - board = link.dataset.board + boardID = link.dataset.boardid threadID = link.dataset.threadid or 0 postID = link.dataset.postid return { - board: board + boardID: boardID threadID: +threadID postID: +postID } @@ -2271,20 +2353,20 @@ Get = # Third: # Filter out irrelevant quotelinks. quotelinks.filter (quotelink) -> - {board, postID} = Get.postDataFromLink quotelink - board is post.board.ID and postID is post.ID - postClone: (board, threadID, postID, root, context) -> - if post = g.posts["#{board}.#{postID}"] + {boardID, postID} = Get.postDataFromLink quotelink + boardID is post.board.ID and postID is post.ID + postClone: (boardID, threadID, postID, root, context) -> + if post = g.posts["#{boardID}.#{postID}"] Get.insert post, root, context return root.textContent = "Loading post No.#{postID}..." if threadID - $.cache "//api.4chan.org/#{board}/res/#{threadID}.json", -> - Get.fetchedPost @, board, threadID, postID, root, context - else if url = Redirect.post board, postID + $.cache "//api.4chan.org/#{boardID}/res/#{threadID}.json", -> + Get.fetchedPost @, boardID, threadID, postID, root, context + else if url = Redirect.post boardID, postID $.cache url, -> - Get.archivedPost @, board, postID, root, context + Get.archivedPost @, boardID, postID, root, context insert: (post, root, context) -> # Stop here if the container has been removed while loading. return unless root.parentNode @@ -2298,19 +2380,19 @@ Get = root.innerHTML = null $.add root, nodes.root - fetchedPost: (req, board, threadID, postID, root, context) -> + fetchedPost: (req, boardID, threadID, postID, root, context) -> # In case of multiple callbacks for the same request, # don't parse the same original post more than once. - if post = g.posts["#{board}.#{postID}"] + if post = g.posts["#{boardID}.#{postID}"] Get.insert post, root, context return {status} = req unless [200, 304].contains status # The thread can die by the time we check a quote. - if url = Redirect.post board, postID + if url = Redirect.post boardID, postID $.cache url, -> - Get.archivedPost @, board, postID, root, context + Get.archivedPost @, boardID, postID, root, context else $.addClass root, 'warning' root.textContent = @@ -2321,30 +2403,30 @@ Get = return posts = JSON.parse(req.response).posts - Build.spoilerRange[board] = posts[0].custom_spoiler + Build.spoilerRange[boardID] = posts[0].custom_spoiler for post in posts break if post.no is postID # we found it! if post.no > postID # The post can be deleted by the time we check a quote. - if url = Redirect.post board, postID + if url = Redirect.post boardID, postID $.cache url, -> - Get.archivedPost @, board, postID, root, context + Get.archivedPost @, boardID, postID, root, context else $.addClass root, 'warning' root.textContent = "Post No.#{postID} was not found." return - board = g.boards[board] or - new Board board - thread = g.threads["#{board}.#{threadID}"] or + board = g.boards[boardID] or + new Board boardID + thread = g.threads["#{boardID}.#{threadID}"] or new Thread threadID, board - post = new Post Build.postFromObject(post, board), thread, board + post = new Post Build.postFromObject(post, boardID), thread, board Main.callbackNodes Post, [post] Get.insert post, root, context - archivedPost: (req, board, postID, root, context) -> + archivedPost: (req, boardID, postID, root, context) -> # In case of multiple callbacks for the same request, # don't parse the same original post more than once. - if post = g.posts["#{board}.#{postID}"] + if post = g.posts["#{boardID}.#{postID}"] Get.insert post, root, context return @@ -2401,7 +2483,7 @@ Get = # id postID: "#{postID}" threadID: "#{threadID}" - board: board + boardID: boardID # info name: data.name_processed capcode: switch data.capcode @@ -2427,43 +2509,20 @@ Get = width: data.media.media_w MD5: data.media.media_hash size: data.media.media_size - turl: data.media.thumb_link or "//thumbs.4chan.org/#{board}/thumb/#{data.media.preview_orig}" + turl: data.media.thumb_link or "//thumbs.4chan.org/#{boardID}/thumb/#{data.media.preview_orig}" theight: data.media.preview_h twidth: data.media.preview_w isSpoiler: data.media.spoiler is '1' - board = g.boards[board] or - new Board board - thread = g.threads["#{board}.#{threadID}"] or + board = g.boards[boardID] or + new Board boardID + thread = g.threads["#{boardID}.#{threadID}"] or new Thread threadID, board post = new Post Build.post(o, true), thread, board, isArchived: true Main.callbackNodes Post, [post] Get.insert post, root, context -Misc = # super semantic - clearThreads: (key) -> - return unless data = $.get key - - unless Object.keys(data.threads).length - $.delete key - return - - return if data.lastChecked > Date.now() - 12 * $.HOUR - - $.ajax "//api.4chan.org/#{g.BOARD}/threads.json", onload: -> - threads = {} - for page in JSON.parse @response - for thread in page.threads - if thread.no of data.threads - threads[thread.no] = data.threads[thread.no] - unless Object.keys(threads).length - $.delete key - return - data.threads = threads - data.lastChecked = Date.now() - $.set key, data - Quotify = init: -> return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes'] @@ -2484,13 +2543,13 @@ Quotify = continue quote = deadlink.textContent - continue unless ID = quote.match(/\d+$/)?[0] - board = + continue unless postID = quote.match(/\d+$/)?[0] + boardID = if m = quote.match /^>>>\/([a-z\d]+)/ m[1] else @board.ID - quoteID = "#{board}.#{ID}" + quoteID = "#{boardID}.#{postID}" # \u00A0 is nbsp if post = g.posts[quoteID] @@ -2498,31 +2557,31 @@ Quotify = # Don't (Dead) when quotifying in an archived post, # and we know the post still exists. a = $.el 'a', - href: "/#{board}/#{post.thread}/res/#p#{ID}" + href: "/#{boardID}/#{post.thread}/res/#p#{postID}" className: 'quotelink' textContent: quote else # Replace the .deadlink span if we can redirect. a = $.el 'a', - href: "/#{board}/#{post.thread}/res/#p#{ID}" + href: "/#{boardID}/#{post.thread}/res/#p#{postID}" className: 'quotelink deadlink' target: '_blank' textContent: "#{quote}\u00A0(Dead)" - a.setAttribute 'data-board', board + a.setAttribute 'data-boardid', boardID a.setAttribute 'data-threadid', post.thread.ID - a.setAttribute 'data-postid', ID - else if redirect = Redirect.to {board, threadID: 0, postID: ID} + a.setAttribute 'data-postid', postID + else if redirect = Redirect.to {boardID, threadID: 0, postID} # Replace the .deadlink span if we can redirect. a = $.el 'a', href: redirect className: 'deadlink' target: '_blank' textContent: "#{quote}\u00A0(Dead)" - if Redirect.post board, ID + if Redirect.post boardID, postID # Make it function as a normal quote if we can fetch the post. $.addClass a, 'quotelink' - a.setAttribute 'data-board', board - a.setAttribute 'data-postid', ID + a.setAttribute 'data-boardid', boardID + a.setAttribute 'data-postid', postID unless @quotes.contains quoteID @quotes.push quoteID @@ -2547,21 +2606,19 @@ QuoteInline = name: 'Quote Inlining' cb: @node node: -> - for link in @nodes.quotelinks - $.on link, 'click', QuoteInline.toggle - for link in @nodes.backlinks + for link in @nodes.quotelinks.concat [@nodes.backlinks...] $.on link, 'click', QuoteInline.toggle return toggle: (e) -> return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 e.preventDefault() - {board, threadID, postID} = Get.postDataFromLink @ + {boardID, threadID, postID} = Get.postDataFromLink @ context = Get.contextFromLink @ if $.hasClass @, 'inlined' - QuoteInline.rm @, board, threadID, postID, context + QuoteInline.rm @, boardID, threadID, postID, context else return if $.x "ancestor::div[@id='p#{postID}']", @ - QuoteInline.add @, board, threadID, postID, context + QuoteInline.add @, boardID, threadID, postID, context @classList.toggle 'inlined' findRoot: (quotelink, isBacklink) -> @@ -2569,15 +2626,15 @@ QuoteInline = quotelink.parentNode.parentNode else $.x 'ancestor-or-self::*[parent::blockquote][1]', quotelink - add: (quotelink, board, threadID, postID, context) -> + add: (quotelink, boardID, threadID, postID, context) -> isBacklink = $.hasClass quotelink, 'backlink' inline = $.el 'div', id: "i#{postID}" className: 'inline' $.after QuoteInline.findRoot(quotelink, isBacklink), inline - Get.postClone board, threadID, postID, inline, context + Get.postClone boardID, threadID, postID, inline, context - return unless (post = g.posts["#{board}.#{postID}"]) and + return unless (post = g.posts["#{boardID}.#{postID}"]) and context.thread is post.thread # Hide forward post if it's a backlink of a post in this thread. @@ -2586,12 +2643,12 @@ QuoteInline = $.addClass post.nodes.root, 'forwarded' post.forwarded++ or post.forwarded = 1 - # Decrease the unread count if this post is in the array of unread posts. - if Unread.posts and (i = Unread.posts.indexOf post) isnt -1 - Unread.posts.splice i, 1 - Unread.update() + # Decrease the unread count if this post + # is in the array of unread posts. + return unless Unread.posts + Unread.readSinglePost post - rm: (quotelink, board, threadID, postID, context) -> + rm: (quotelink, boardID, threadID, postID, context) -> isBacklink = $.hasClass quotelink, 'backlink' # Select the corresponding inlined quote, and remove it. root = QuoteInline.findRoot quotelink, isBacklink @@ -2602,21 +2659,21 @@ QuoteInline = return unless el = root.firstElementChild # Dereference clone. - post = g.posts["#{board}.#{postID}"] + post = g.posts["#{boardID}.#{postID}"] post.rmClone el.dataset.clone # Decrease forward count and unhide. if Conf['Forward Hiding'] and isBacklink and - context.thread is g.threads["#{board}.#{threadID}"] and + context.thread is g.threads["#{boardID}.#{threadID}"] and not --post.forwarded delete post.forwarded $.rmClass post.nodes.root, 'forwarded' # Repeat. while inlined = $ '.inlined', el - {board, threadID, postID} = Get.postDataFromLink inlined - QuoteInline.rm inlined, board, threadID, postID, context + {boardID, threadID, postID} = Get.postDataFromLink inlined + QuoteInline.rm inlined, boardID, threadID, postID, context $.rmClass inlined, 'inlined' return @@ -2631,21 +2688,19 @@ QuotePreview = name: 'Quote Previewing' cb: @node node: -> - for link in @nodes.quotelinks - $.on link, 'mouseover', QuotePreview.mouseover - for link in @nodes.backlinks + for link in @nodes.quotelinks.concat [@nodes.backlinks...] $.on link, 'mouseover', QuotePreview.mouseover return mouseover: (e) -> return if $.hasClass @, 'inlined' - {board, threadID, postID} = Get.postDataFromLink @ + {boardID, threadID, postID} = Get.postDataFromLink @ qp = $.el 'div', id: 'qp' className: 'dialog' $.add d.body, qp - Get.postClone board, threadID, postID, qp, Get.contextFromLink @ + Get.postClone boardID, threadID, postID, qp, Get.contextFromLink @ UI.hover root: @ @@ -2655,7 +2710,7 @@ QuotePreview = cb: QuotePreview.mouseout asapTest: -> qp.firstElementChild - return unless origin = g.posts["#{board}.#{postID}"] + return unless origin = g.posts["#{boardID}.#{postID}"] if Conf['Quote Highlighting'] posts = [origin].concat origin.clones @@ -2666,10 +2721,7 @@ QuotePreview = quoterID = $.x('ancestor::*[@id][1]', @).id.match(/\d+$/)[0] clone = Get.postFromRoot qp.firstChild - for quote in clone.nodes.quotelinks - if quote.hash[2..] is quoterID - $.addClass quote, 'forwardlink' - for quote in clone.nodes.backlinks + for quote in clone.nodes.quotelinks.concat [clone.nodes.backlinks...] if quote.hash[2..] is quoterID $.addClass quote, 'forwardlink' return @@ -2761,8 +2813,7 @@ QuoteYou = {quotelinks} = @nodes for quotelink in quotelinks - {threadID, postID} = Get.postDataFromLink quotelink - if (thread = QR.yourPosts.threads[threadID]) and thread.contains postID + if QR.db.get Get.postDataFromLink quotelink $.add quotelink, $.tn QuoteYou.text return @@ -2794,8 +2845,8 @@ QuoteOP = # add (OP) to quotes quoting this context's OP. return unless quotes.contains op for quotelink in quotelinks - {board, postID} = Get.postDataFromLink quotelink - if "#{board}.#{postID}" is op + {boardID, postID} = Get.postDataFromLink quotelink + if "#{boardID}.#{postID}" is op $.add quotelink, $.tn QuoteOP.text return @@ -2820,11 +2871,11 @@ QuoteCT = {board, thread} = if @isClone then @context else @ for quotelink in quotelinks - data = Get.postDataFromLink quotelink - continue unless data.threadID # deadlink + {boardID, threadID} = Get.postDataFromLink quotelink + continue unless threadID # deadlink if @isClone quotelink.textContent = quotelink.textContent.replace QuoteCT.text, '' - if data.board is @board.ID and data.threadID isnt thread.ID + if boardID is @board.ID and threadID isnt thread.ID $.add quotelink, $.tn QuoteCT.text return @@ -3126,7 +3177,7 @@ ImageExpand = @EAI = wrapper.firstElementChild $.on @EAI, 'click', ImageExpand.cb.toggleAll - @menu = new UI.Menu 'imageexpand' + @opmenu = new UI.Menu 'imageexpand' $.on $('.menu-button', wrapper), 'click', @menuToggle for type, config of Config.imageExpansion @@ -3199,20 +3250,16 @@ ImageExpand = unless post.file.isExpanded or $.hasClass thumb, 'expanding' ImageExpand.expand post return - rect = thumb.parentNode.getBoundingClientRect() - if rect.bottom > 0 # Should be at least partially visible. - # Scroll back to the thumbnail when contracting the image - # to avoid being left miles away from the relevant post. - postRect = post.nodes.root.getBoundingClientRect() - headRect = Header.toggle.getBoundingClientRect() - top = postRect.top - headRect.top - headRect.height - 2 - root = if $.engine is 'webkit' - d.body - else - doc - root.scrollTop += top if rect.top < 0 - root.scrollLeft = 0 if rect.left < 0 ImageExpand.contract post + rect = post.nodes.root.getBoundingClientRect() + return unless rect.top <= 0 or rect.left <= 0 + # Scroll back to the thumbnail when contracting the image + # to avoid being left miles away from the relevant post. + headRect = Header.bar.getBoundingClientRect() + top = rect.top - headRect.top - headRect.height + root = if $.engine is 'webkit' then d.body else doc + root.scrollTop += top if rect.top < 0 + root.scrollLeft = 0 if rect.left < 0 contract: (post) -> $.rmClass post.nodes.root, 'expanded-image' @@ -3240,16 +3287,21 @@ ImageExpand = completeExpand: (post) -> {thumb} = post.file return unless $.hasClass thumb, 'expanding' # contracted before the image loaded - rect = post.nodes.root.getBoundingClientRect() - $.addClass post.nodes.root, 'expanded-image' - $.rmClass post.file.thumb, 'expanding' - if rect.top + rect.height <= 0 - root = if $.engine is 'webkit' - d.body - else - doc - root.scrollTop += post.nodes.root.clientHeight - rect.height post.file.isExpanded = true + unless post.nodes.root.parentNode + # Image might start/finish loading before the post is inserted. + # Don't scroll when it's expanded in a QP for example. + $.addClass post.nodes.root, 'expanded-image' + $.rmClass post.file.thumb, 'expanding' + return + prev = post.nodes.root.getBoundingClientRect() + $.queueTask -> + $.addClass post.nodes.root, 'expanded-image' + $.rmClass post.file.thumb, 'expanding' + return unless prev.top + prev.height <= 0 + root = if $.engine is 'webkit' then d.body else doc + curr = post.nodes.root.getBoundingClientRect() + root.scrollTop += curr.height - prev.height + curr.top - prev.top error: -> post = Get.postFromNode @ @@ -3284,11 +3336,43 @@ ImageExpand = clearTimeout timeoutID post.kill true + menu: + init: -> + return if g.VIEW is 'catalog' or !Conf['Image Expansion'] + + el = $.el 'span', + textContent: 'Image Expansion' + className: 'image-expansion-link' + + {createSubEntry} = ImageExpand.menu + subEntries = [] + for key, conf of Config.imageExpansion + subEntries.push createSubEntry key, conf + + $.event 'AddMenuEntry', + type: 'header' + el: el + order: 80 + subEntries: subEntries + + createSubEntry: (type, config) -> + label = $.el 'label', + innerHTML: " #{type}" + input = label.firstElementChild + if type in ['Fit width', 'Fit height'] + $.on input, 'change', ImageExpand.cb.setFitness + if config + label.title = config[1] + input.checked = Conf[type] + $.event 'change', null, input + $.on input, 'change', $.cb.checked + el: label + resize: -> ImageExpand.style.textContent = ":root.fit-height .full-image {max-height:#{doc.clientHeight}px}" menuToggle: (e) -> - ImageExpand.menu.toggle e, @, g + ImageExpand.opmenu.toggle e, @, g RevealSpoilers = init: -> @@ -3462,36 +3546,44 @@ ExpandThread = toggle: (thread) -> threadRoot = thread.OP.nodes.root.parentNode - url = "//api.4chan.org/#{thread.board}/res/#{thread}.json" - a = $ '.summary', threadRoot + a = $ '.summary', threadRoot - text = a.textContent - switch text[0] - when '+' - a.textContent = text.replace '+', '× Loading...' - $.cache url, -> ExpandThread.parse @, thread, a + switch thread.isExpanded + when false, undefined + thread.isExpanded = 'loading' for post in $$ '.thread > .postContainer', threadRoot ExpandComment.expand Get.postFromRoot post + unless a + thread.isExpanded = true + return + thread.isExpanded = 'loading' + a.textContent = a.textContent.replace '+', '× Loading...' + $.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", -> + ExpandThread.parse @, thread, a - when '×' - a.textContent = text.replace '× Loading...', '+' + when 'loading' + thread.isExpanded = false + return unless a + a.textContent = a.textContent.replace '× Loading...', '+' - when '-' - a.textContent = text.replace '-', '+' - #goddamit moot - num = if thread.isSticky - 1 - else switch g.BOARD.ID - # XXX boards config - when 'b', 'vg', 'q' then 3 - when 't' then 1 - else 5 - replies = $$('.thread > .replyContainer', threadRoot)[...-num] - for reply in replies - if Conf['Quote Inlining'] - # rm clones - inlined.click() while inlined = $ '.inlined', reply - $.rm reply + when true + thread.isExpanded = false + if a + a.textContent = a.textContent.replace '-', '+' + #goddamit moot + num = if thread.isSticky + 1 + else switch g.BOARD.ID + # XXX boards config + when 'b', 'vg', 'q' then 3 + when 't' then 1 + else 5 + replies = $$('.thread > .replyContainer', threadRoot)[...-num] + for reply in replies + if Conf['Quote Inlining'] + # rm clones + inlined.click() while inlined = $ '.inlined', reply + $.rm reply for post in $$ '.thread > .postContainer', threadRoot ExpandComment.contract Get.postFromRoot post return @@ -3504,6 +3596,7 @@ ExpandThread = $.off a, 'click', ExpandThread.cb.toggle return + thread.isExpanded = true a.textContent = a.textContent.replace '× Loading...', '-' posts = JSON.parse(req.response).posts @@ -3550,24 +3643,30 @@ Unread = init: -> return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Tab Icon'] - Unread.hr = $.el 'hr', + @db = new DataBoard 'lastReadPosts', @sync + @hr = $.el 'hr', id: 'unread-line' - Misc.clearThreads "lastReadPosts.#{g.BOARD}" + @posts = [] + @postsQuotingYou = [] + Thread::callbacks.push name: 'Unread' cb: @node node: -> - Unread.thread = @ - Unread.lastReadPost = $.get("lastReadPosts.#{@board}", threads: {}).threads[@] or 0 - Unread.posts = [] - Unread.postsQuotingYou = [] - Unread.title = d.title + Unread.thread = @ + Unread.title = d.title posts = [] for ID, post of @posts posts.push post if post.isReply + Unread.lastReadPost = Unread.db.get + boardID: @board.ID + threadID: @ID + defaultValue: 0 Unread.addPosts posts - if Unread.posts.length + if (hash = location.hash.match /\d+/) and post = @posts[hash[0]] + post.nodes.root.scrollIntoView() + else if Unread.posts.length # Scroll to before the first unread post. $.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView false else if posts.length @@ -3577,30 +3676,43 @@ Unread = $.on d, 'scroll visibilitychange', Unread.read $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] + sync: -> + lastReadPost = Unread.db.get + boardID: Unread.thread.board.ID + threadID: Unread.thread.ID + defaultValue: 0 + return unless Unread.lastReadPost < lastReadPost + Unread.lastReadPost = lastReadPost + Unread.readArray Unread.posts + Unread.readArray Unread.postsQuotingYou + Unread.setLine() + Unread.update() + addPosts: (newPosts) -> - if Conf['Quick Reply'] - {yourPosts} = QR - youInThisThread = yourPosts.threads[Unread.thread] for post in newPosts {ID} = post - if ID <= Unread.lastReadPost or post.isHidden or youInThisThread and youInThisThread.contains ID + if ID <= Unread.lastReadPost or post.isHidden continue + if QR.db + data = + boardID: post.board.ID + threadID: post.thread.ID + postID: post.ID + continue if QR.db.get data Unread.posts.push post - Unread.addPostQuotingYou post, yourPosts if yourPosts + Unread.addPostQuotingYou post if Conf['Unread Line'] # Force line on visible threads if there were no unread posts previously. Unread.setLine newPosts.contains Unread.posts[0] Unread.read() Unread.update() - addPostQuotingYou: (post, yourPosts) -> - for quote in post.quotes - [board, quoteID] = quote.split '.' - continue unless board is Unread.thread.board.ID - for thread, postIDs of yourPosts.threads - if postIDs.contains +quoteID - Unread.postsQuotingYou.push post - return + addPostQuotingYou: (post) -> + return unless QR.db + for quotelink in post.nodes.quotelinks + if QR.db.get Get.postDataFromLink quotelink + Unread.postsQuotingYou.push post + return onUpdate: (e) -> if e.detail[404] @@ -3608,6 +3720,21 @@ Unread = else Unread.addPosts e.detail.newPosts + readSinglePost: (post) -> + return if (i = Unread.posts.indexOf post) is -1 + Unread.posts.splice i, 1 + if i is 0 + Unread.lastReadPost = post.ID + Unread.saveLastReadPost() + if (i = Unread.postsQuotingYou.indexOf post) isnt -1 + Unread.postsQuotingYou.splice i, 1 + Unread.update() + + readArray: (arr) -> + for post, i in arr + break if post.ID > Unread.lastReadPost + arr.splice 0, i + read: (e) -> return if d.hidden or !Unread.posts.length height = doc.clientHeight @@ -3618,17 +3745,15 @@ Unread = Unread.lastReadPost = Unread.posts[i - 1].ID Unread.saveLastReadPost() - Unread.posts = Unread.posts[i..] - for post, i in Unread.postsQuotingYou - break if post.ID > Unread.lastReadPost - Unread.postsQuotingYou = Unread.postsQuotingYou[i..] + Unread.posts.splice 0, i + Unread.readArray Unread.postsQuotingYou Unread.update() if e - saveLastReadPost: $.debounce($.SECOND, -> - lastReadPosts = $.get "lastReadPosts.#{Unread.thread.board}", threads: {} - lastReadPosts.threads[Unread.thread] = Unread.lastReadPost - $.set "lastReadPosts.#{Unread.thread.board}", lastReadPosts - ) + saveLastReadPost: $.debounce 2 * $.SECOND, -> + Unread.db.set + boardID: Unread.thread.board.ID + threadID: Unread.thread.ID + val: Unread.lastReadPost setLine: (force) -> return unless d.hidden or force is true @@ -3639,7 +3764,7 @@ Unread = else if Unread.hr.parentNode $.rm Unread.hr - update: -> + update: <% if (type === 'crx') { %>(dontrepeat) <% } %>-> count = Unread.posts.length if Conf['Unread Count'] @@ -3647,6 +3772,17 @@ Unread = "(#{Unread.posts.length}) /#{g.BOARD}/ - 404" else "(#{Unread.posts.length}) #{Unread.title}" + <% if (type === 'crx') { %> + # XXX Chrome bug where it doesn't always update the tab title. + # crbug.com/124381 + # Call it one second later, + # but don't display outdated unread count. + unless dontrepeat + setTimeout -> + d.title = '' + Unread.update true + , $.SECOND + <% } %> return unless Conf['Unread Tab Icon'] @@ -3659,10 +3795,11 @@ Unread = else Favicon.dead else - if Unread.postsQuotingYou.length - Favicon.unreadY - else if count - Favicon.unread + if count + if Unread.postsQuotingYou.length + Favicon.unreadY + else + Favicon.unread else Favicon.default @@ -3743,19 +3880,20 @@ ThreadStats = for ID, post of @posts postCount++ fileCount++ if post.file - ThreadStats.update postCount, fileCount ThreadStats.thread = @ + ThreadStats.update postCount, fileCount $.on d, 'ThreadUpdate', ThreadStats.onUpdate $.add d.body, ThreadStats.dialog onUpdate: (e) -> return if e.detail[404] - {postCount, fileCount, postLimit, fileLimit} = e.detail - ThreadStats.update postCount, fileCount, postLimit, fileLimit - update: (postCount, fileCount, postLimit, fileLimit) -> - ThreadStats.postCountEl.textContent = postCount - ThreadStats.fileCountEl.textContent = fileCount - (if postLimit and !ThreadStats.thread.isSticky then $.addClass else $.rmClass) ThreadStats.postCountEl, 'warning' - (if fileLimit and !ThreadStats.thread.isSticky then $.addClass else $.rmClass) ThreadStats.fileCountEl, 'warning' + {postCount, fileCount} = e.detail + ThreadStats.update postCount, fileCount + update: (postCount, fileCount) -> + {thread, postCountEl, fileCountEl} = ThreadStats + postCountEl.textContent = postCount + fileCountEl.textContent = fileCount + (if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning' + (if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning' ThreadUpdater = init: -> @@ -3974,6 +4112,8 @@ ThreadUpdater = ThreadUpdater.updateThreadStatus 'Sticky', OP ThreadUpdater.updateThreadStatus 'Closed', OP + ThreadUpdater.thread.postLimit = !!OP.bumplimit + ThreadUpdater.thread.fileLimit = !!OP.imagelimit nodes = [] # post container elements posts = [] # post objects @@ -4052,8 +4192,6 @@ ThreadUpdater = deletedFiles: deletedFiles postCount: OP.replies + 1 fileCount: OP.images + (!!ThreadUpdater.thread.OP.file and !ThreadUpdater.thread.OP.file.isDead) - postLimit: !!OP.bumplimit - fileLimit: !!OP.imagelimit ThreadWatcher = init: -> @@ -4074,7 +4212,9 @@ ThreadWatcher = className: 'favicon' $.on favicon, 'click', ThreadWatcher.cb.toggle $.before $('input', @OP.nodes.post), favicon - if g.VIEW is 'thread' and @ID is $.get 'AutoWatch', 0 + return if g.VIEW isnt 'thread' + $.get 'AutoWatch', 0, (item) => + return if item['AutoWatch'] isnt @ID ThreadWatcher.watch @ $.delete 'AutoWatch' @@ -4084,7 +4224,10 @@ ThreadWatcher = $.add d.body, ThreadWatcher.dialog refresh: (watched) -> - watched or= $.get 'WatchedThreads', {} + unless watched + $.get 'WatchedThreads', {}, (item) -> + ThreadWatcher.refresh item['WatchedThreads'] + return nodes = [$('.move', ThreadWatcher.dialog)] for board of watched for id, props of watched[board] @@ -4132,20 +4275,22 @@ ThreadWatcher = ThreadWatcher.unwatch thread.board, thread.ID unwatch: (board, threadID) -> - watched = $.get 'WatchedThreads', {} - delete watched[board][threadID] - delete watched[board] unless Object.keys(watched[board]).length - ThreadWatcher.refresh watched - $.set 'WatchedThreads', watched + $.get 'WatchedThreads', {}, (item) -> + watched = item['WatchedThreads'] + delete watched[board][threadID] + delete watched[board] unless Object.keys(watched[board]).length + ThreadWatcher.refresh watched + $.set 'WatchedThreads', watched watch: (thread) -> - watched = $.get 'WatchedThreads', {} - watched[thread.board] or= {} - watched[thread.board][thread] = - href: "/#{thread.board}/res/#{thread}" - textContent: Get.threadExcerpt thread - ThreadWatcher.refresh watched - $.set 'WatchedThreads', watched + $.get 'WatchedThreads', {}, (item) -> + watched = item['WatchedThreads'] + watched[thread.board] or= {} + watched[thread.board][thread] = + href: "/#{thread.board}/res/#{thread}" + textContent: Get.threadExcerpt thread + ThreadWatcher.refresh watched + $.set 'WatchedThreads', watched Linkify = init: -> diff --git a/src/main.coffee b/src/main.coffee index 4abfefcb6..00d702f57 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -16,7 +16,7 @@ class Thread @fullID = "#{@board}.#{@ID}" @posts = {} - g.threads["#{board}.#{@}"] = board.threads[@] = @ + g.threads[@fullID] = board.threads[@] = @ kill: -> @isDead = true @@ -66,7 +66,11 @@ class Post @nodes.date = date @info.date = new Date date.dataset.utc * 1000 if Conf['Quick Reply'] - @info.yours = !!QR.yourPosts.threads[@thread.ID]?.contains(@ID) + @info.yours = QR.db.get + boardID: @board + threadID: @thread + postID: @ID + @parseComment() @parseQuotes() @@ -109,7 +113,7 @@ class Post @thread.isClosed = !!$ '.closedIcon', @nodes.info @clones = [] - g.posts["#{board}.#{@}"] = thread.posts[@] = board.posts[@] = @ + g.posts[@fullID] = thread.posts[@] = board.posts[@] = @ @kill() if that.isArchived parseComment: -> @@ -160,10 +164,12 @@ class Post kill: (file, now) -> now or= new Date() if file + return if @file.isDead @file.isDead = true @file.timeOfDeath = now $.addClass @nodes.root, 'deleted-file' else + return if @isDead @isDead = true @timeOfDeath = now $.addClass @nodes.root, 'deleted-post' @@ -283,7 +289,7 @@ class Clone extends Post Main = - init: -> + init: (items) -> # flatten Config into Conf # and get saved or default values flatten = (parent, obj) -> @@ -296,8 +302,14 @@ Main = Conf[parent] = obj return flatten null, Config - for key, val of Conf - Conf[key] = $.get key, val + for db in DataBoards + Conf[db] = boards: {} + $.get Conf, Main.initFeatures + + $.on d, '4chanMainInit', Main.initStyle + + initFeatures: (items) -> + Conf = items pathname = location.pathname.split '/' g.BOARD = new Board pathname[1] @@ -310,7 +322,7 @@ Main = else 'index' if g.VIEW is 'thread' - g.THREAD = +pathname[3] + g.THREADID = +pathname[3] # Check if the current board we're on is SFW or not, so we can handle options that need to know that. if ['b', 'd', 'e', 'gif', 'h', 'hc', 'hm', 'hr', 'pol', 'r', 'r9k', 'rs', 's', 'soc', 't', 'u', 'y'].contains g.BOARD @@ -351,6 +363,7 @@ Main = return # c.time 'All initializations' + initFeatures 'Polyfill': Polyfill 'Emoji': Emoji @@ -367,14 +380,14 @@ Main = 'Resurrect Quotes': Quotify 'Filter': Filter 'Thread Hiding': ThreadHiding - 'Reply Hiding': ReplyHiding + 'Reply Hiding': PostHiding 'Recursive': Recursive 'Strike-through Quotes': QuoteStrikeThrough 'Quick Reply': QR 'Menu': Menu 'Report Link': ReportLink 'Thread Hiding (Menu)': ThreadHiding.menu - 'Reply Hiding (Menu)': ReplyHiding.menu + 'Reply Hiding (Menu)': PostHiding.menu 'Delete Link': DeleteLink 'Filter (Menu)': Filter.menu 'Download Link': DownloadLink @@ -391,6 +404,7 @@ Main = 'File Info Formatting': FileInfo 'Sauce': Sauce 'Image Expansion': ImageExpand + 'Image Expansion (Menu)': ImageExpand.menu 'Reveal Spoilers': RevealSpoilers 'Image Replace': ImageReplace 'Image Hover': ImageHover @@ -404,6 +418,7 @@ Main = 'Thread Watcher': ThreadWatcher 'Index Navigation': Nav 'Keybinds': Keybinds + # c.timeEnd 'All initializations' $.on d, 'AddCallback', Main.addCallback @@ -413,9 +428,9 @@ Main = if d.title is '4chan - 404 Not Found' if Conf['404 Redirect'] and g.VIEW is 'thread' href = Redirect.to - board: g.BOARD - threadID: g.THREAD - postID: location.hash + boardID: g.BOARD.ID + threadID: g.THREADID + postID: +location.hash.match /\d+/ # post number or 0 location.href = href or "/#{g.BOARD}/" return @@ -479,30 +494,34 @@ Main = Klass::callbacks.push obj.callback checkUpdate: -> - return unless Main.isThisPageLegit() + return unless Conf['Check for Updates'] and Main.isThisPageLegit() # Check for updates after: # - 6 hours since the last update on Opera because it lacks auto-updating. # - 7 days since the last update on Chrome/Firefox. # After that, check for updates every day if we still haven't updated. now = Date.now() freq = <% if (type === 'userjs') { %>6 * $.HOUR<% } else { %>7 * $.DAY<% } %> - if $.get('lastupdate', 0) > now - freq or $.get('lastchecked', 0) > now - $.DAY - return - $.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: -> - return unless @status is 200 - version = @response - return unless /^\d\.\d+\.\d+$/.test version - if g.VERSION is version - # Don't check for updates too frequently if there wasn't one in a 'long' time. - $.set 'lastupdate', now + items = + lastupdate: 0 + lastchecked: 0 + $.get items, (items) -> + if items.lastupdate > now - freq or items.lastchecked > now - $.DAY return - $.set 'lastchecked', now - el = $.el 'span', - innerHTML: "Update: <%= meta.name %> v#{version} is out, get it target=_blank>here." - new Notification 'info', el, 2 * $.MINUTE + $.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: -> + return unless @status is 200 + version = @response + return unless /^\d\.\d+\.\d+$/.test version + if g.VERSION is version + # Don't check for updates too frequently if there wasn't one in a 'long' time. + $.set 'lastupdate', now + return + $.set 'lastchecked', now + el = $.el 'span', + innerHTML: "Update: <%= meta.name %> v#{version} is out, get it target=_blank>here." + new Notification 'info', el, 120 handleErrors: (errors) -> - unless 'length' of errors + unless errors instanceof Array error = errors else if errors.length is 1 error = errors[0] @@ -513,12 +532,10 @@ Main = div = $.el 'div', innerHTML: "#{errors.length} errors occurred. [show]" $.on div.lastElementChild, 'click', -> - if @textContent is 'show' - @textContent = 'hide' - logs.hidden = false + [@textContent, logs.hidden] = if @textContent is 'show' + ['hide', false] else - @textContent = 'show' - logs.hidden = true + ['show', true] logs = $.el 'div', hidden: true @@ -528,15 +545,31 @@ Main = new Notification 'error', [div, logs], 30 parseError: (data) -> - {message, error} = data - c.log message, error - c.log message, error.stack + Main.logError data message = $.el 'div', - textContent: message + textContent: data.message error = $.el 'div', - textContent: error + textContent: data.error [message, error] + errors: [] + logError: (data) -> + unless Main.errors.length + $.on window, 'unload', Main.postErrors + c.error data.message, data.error.stack + Main.errors.push data + + postErrors: -> + errors = Main.errors.map (d) -> d.message + ' ' + d.error.stack + $.ajax '<%= meta.page %>errors', {}, + sync: true + form: $.formData + n: "<%= meta.name %> v#{g.VERSION}" + t: '<%= type %>' + ua: window.navigator.userAgent + url: window.location.href + e: errors.join '\n' + isThisPageLegit: -> # 404 error page or similar. unless 'thisPageIsLegit' of Main diff --git a/src/manifest.json b/src/manifest.json index b17375947..c89510295 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -14,7 +14,7 @@ "run_at": "document_start" }], "homepage_url": "<%= meta.page %>", - "minimum_chrome_version": "25", + "minimum_chrome_version": "26", "permissions": [ "storage" ] diff --git a/src/qr.coffee b/src/qr.coffee index 56448a468..6edd463d7 100644 --- a/src/qr.coffee +++ b/src/qr.coffee @@ -1,9 +1,8 @@ QR = init: -> - return if g.VIEW is 'catalog' or !Conf['Quick Reply'] + return if !Conf['Quick Reply'] - Misc.clearThreads "yourPosts.#{g.BOARD}" - @syncYourPosts() + @db = new DataBoard 'yourPosts' sc = $.el 'a', className: "qr-shortcut #{unless Conf['Persistent QR'] then 'disabled' else ''}" @@ -93,13 +92,6 @@ QR = else QR.unhide() - syncYourPosts: (yourPosts) -> - if yourPosts - QR.yourPosts = yourPosts - return - QR.yourPosts = $.get "yourPosts.#{g.BOARD}", threads: {} - $.sync "yourPosts.#{g.BOARD}", QR.syncYourPosts - error: (err) -> QR.open() if typeof err is 'string' @@ -150,10 +142,11 @@ QR = sage: if board is 'q' then 600 else 60 file: if board is 'q' then 300 else 30 post: if board is 'q' then 60 else 30 - QR.cooldown.cooldowns = $.get "cooldown.#{board}", {} QR.cooldown.upSpd = 0 QR.cooldown.upSpdAccuracy = .5 - QR.cooldown.start() + $.get "cooldown.#{board}", {}, (item) -> + QR.cooldown.cooldowns = item["cooldown.#{board}"] + QR.cooldown.start() $.sync "cooldown.#{board}", QR.cooldown.sync start: -> return if QR.cooldown.isCounting @@ -356,22 +349,13 @@ QR = $.addClass QR.nodes.el, 'dump' resetThreadSelector: -> if g.VIEW is 'thread' - QR.nodes.thread.value = g.THREAD + QR.nodes.thread.value = g.THREADID else QR.nodes.thread.value = 'new' posts: [] post: class - constructor: -> - # set values, or null, to avoid 'undefined' values in inputs - prev = QR.posts[QR.posts.length - 1] - persona = $.get 'QR.persona', {} - @name = if prev then prev.name else persona.name or null - @email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null - @sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null - @spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false - @com = null - + constructor: (select) -> el = $.el 'a', className: 'qr-preview' draggable: true @@ -398,13 +382,32 @@ QR = for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop'] $.on el, event.toLowerCase(), @[event] - @unlock() + prev = QR.posts[QR.posts.length - 1] QR.posts.push @ + @spoiler = if prev and Conf['Remember Spoiler'] + prev.spoiler + else + false + $.get 'QR.persona', {}, (item) => + persona = item['QR.persona'] + @name = if prev + prev.name + else + persona.name + @email = if prev and !/^sage$/.test prev.email + prev.email + else + persona.email + if Conf['Remember Subject'] + @sub = if prev then prev.sub else persona.sub + @load() if QR.selected is @ # load persona + @select() if select + @unlock() rm: -> $.rm @nodes.el index = QR.posts.indexOf @ if QR.posts.length is 1 - new QR.post().select() + new QR.post true else if @ is QR.selected (QR.posts[index-1] or QR.posts[index+1]).select() QR.posts.splice index, 1 @@ -433,9 +436,11 @@ QR = rectEl = @nodes.el.getBoundingClientRect() rectList = @nodes.el.parentNode.getBoundingClientRect() @nodes.el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2 + @load() + load: -> # Load this post's values. for name in ['name', 'email', 'sub', 'com'] - QR.nodes[name].value = @[name] + QR.nodes[name].value = @[name] or null @showFileData() QR.characterCount() save: (input) -> @@ -586,7 +591,6 @@ QR = $.on window, 'captcha:timeout', setLifetime $.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))' $.off window, 'captcha:timeout', setLifetime - c.log @lifetime imgContainer = $.el 'div', className: 'captcha-img' @@ -611,8 +615,9 @@ QR = $.on imgContainer, 'click', @reload.bind @ $.on input, 'keydown', @keydown.bind @ + $.get 'captchas', [], (item) => + @sync item['captchas'] $.sync 'captchas', @sync - @sync $.get 'captchas', [] # start with an uncached captcha @reload() @@ -794,13 +799,13 @@ QR = $.on nodes.autohide, 'change', QR.toggleHide $.on nodes.close, 'click', QR.close $.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump' - $.on nodes.addPost, 'click', -> new QR.post().select() + $.on nodes.addPost, 'click', -> new QR.post true $.on nodes.form, 'submit', QR.submit $.on nodes.fileRM, 'click', -> QR.selected.rmFile() $.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click() $.on nodes.fileInput, 'change', QR.fileInput - new QR.post().select() + new QR.post true # save selected post's data for name in ['name', 'email', 'sub', 'com'] $.on nodes[name], 'input', -> QR.selected.save @ @@ -839,10 +844,12 @@ QR = err = 'New threads require a subject.' else unless post.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm' err = 'No file selected.' - else if g.BOARD.threads[threadID].isSticky + else if g.BOARD.threads[threadID].isClosed err = 'You can\'t reply to this thread anymore.' else unless post.com or post.file err = 'No file selected.' + else if post.file and g.BOARD.threads[threadID].fileLimit + err = 'Max limit of image replies has been reached.' if QR.captcha.isEnabled and !err {challenge, response} = QR.captcha.getOne() @@ -893,6 +900,7 @@ QR = QR.error $.el 'span', innerHTML: 'Connection error. You may have been banned.' opts = + cred: true form: $.formData postData upCallbacks: onload: -> @@ -965,21 +973,25 @@ QR = QR.cleanNotifications() QR.notifications.push new Notification 'success', h1.textContent, 5 - persona = $.get 'QR.persona', {} - persona = - name: post.name - email: if /^sage$/.test post.email then persona.email else post.email - sub: if Conf['Remember Subject'] then post.sub else null - $.set 'QR.persona', persona + $.get 'QR.persona', {}, (item) -> + persona = item['QR.persona'] + persona = + name: post.name + email: if /^sage$/.test post.email then persona.email else post.email + sub: if Conf['Remember Subject'] then post.sub else null + $.set 'QR.persona', persona [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/ postID = +postID threadID = +threadID or postID isReply = threadID isnt postID - (QR.yourPosts.threads[threadID] or= []).push postID - $.set "yourPosts.#{g.BOARD}", QR.yourPosts - + QR.db.set + boardID: g.BOARD.ID + threadID: threadID + postID: postID + val: true + ThreadUpdater.postID = postID # Post/upload confirmed as successful. @@ -992,7 +1004,10 @@ QR = # Enable auto-posting if we have stuff to post, disable it otherwise. QR.cooldown.auto = QR.posts.length > 1 and isReply - post.rm() + unless Conf['Persistent QR'] or QR.cooldown.auto + QR.close() + else + post.rm() QR.cooldown.set {req, post, isReply} @@ -1006,9 +1021,6 @@ QR = else window.location = "/#{g.BOARD}/res/#{threadID}" - unless Conf['Persistent QR'] or QR.cooldown.auto - QR.close() - QR.status() abort: ->