diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index 1f82c94a4..54edd21ea 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -115,7 +115,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -3207,6 +3207,7 @@ this.isPinned = false; this.isSticky = false; this.isClosed = false; + this.isArchived = false; this.postLimit = false; this.fileLimit = false; this.ipCount = void 0; @@ -12747,6 +12748,19 @@ Favicon = { init: function() { + return $.asap((function() { + return d.head && (Favicon.el = $('link[rel="shortcut icon"]', d.head)); + }), Favicon.initAsap); + }, + initAsap: function() { + var href; + Favicon.el.type = 'image/x-icon'; + href = Favicon.el.href; + Favicon.SFW = /ws\.ico$/.test(href); + Favicon["default"] = href; + return Favicon["switch"](); + }, + "switch": function() { var f, funreadDeadY, i, items, t; items = { ferongr: ['iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///9zBQC/AADpDAP/gID/q6voCwJJTwpOAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxUlEQVR42q1TOwrCQBB9s0FRtJI0WoqFtSLYegoP4gVSeJsUHsHSI3iFeIqRXXgwrhlXwYHHhLwPTB7B36abBCV+0pA4DUBQUNZYQptGtW3jtoKyxgoe0yrBCoyZfL/5ioQ3URZOXW9I341l3oo+NXEZiW4CEuIzvPECopED4OaZ3RNmeAm4u+a8Jr5f17VyVoL8fr8qcltzwlyyj2iqcgPOQ9ExkHAITgD75bYBe0A5S4H/P9htuWMF3QXoQpwaKeT+lnsC6JE5I6aq6fEAAAAASUVORK5CYII=', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8AcH4AtswA2PJ55fKi6fIA1/FtpPADAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxElEQVQ4y2NgoBq4/vE/HJOsBiRQUIfA2AzBqQYqUfn00/9FLz+BaQxDCKqBmX7jExijKEDSDJPHrnnbGQhGV4RmOFwdVkNwhQMheYwQxhaIi7b9Z9A3gWAQm2BUoQOgRhgA8o7j1ozLC4LCyAZcx6kZI5qg4kLKqggDFFWxJySsUQVzlb4pwgAJaTRvokcVNgOqOv8zcHBCsL07DgNg8YsczzA5MxtUL+DMD8g0slxI/H8GQ/P/DJKyeKIRpglXZsIiBwBhP5O+VbI/JgAAAABJRU5ErkJggg==', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8oeQBJ3ABV/wHM/7Lu/+ZU/gAqUP3dAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAx0lEQVQ4y2NgoBYI+cfwH4ZJVgMS0KhEYGyG4FQDkzjzf9P/d/+fgWl0QwiqgSkI/c8IxsgKkDXD5LFq9rwDweiK0A2HqcNqCK5wICSPEcLYAtH+AMN/IXMIBrEJRie6OEgjDAC5x3FqxuUFNiEUA67j1IweTTBxBQ1puAG86jgSEraogskJWSBcwCGF5k30qMJmgMFEhv/MXBAs5oLDAFj8IsczTE7UEeECbhU8+QGZRpaTi2b4L2zF8J9TGk80wjThykzY5AAW/2O1C2mIbgAAAABJRU5ErkJggg=='], @@ -12862,7 +12876,7 @@ ThreadExcerpt = { init: function() { - if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) { + if ((g.BOARD.ID !== 'f' && g.BOARD.ID !== 'pol') || g.VIEW !== 'thread' || !Conf['Thread Excerpt'] || Conf['Remove Thread Excerpt']) { return; } return Thread.callbacks.push({ @@ -12897,7 +12911,10 @@ }); Header.addShortcut(sc); } else { - this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', "
" + html + "
"); + this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', { + innerHTML: "
" + statsHTML.innerHTML + "
" + }); + $.addClass(doc, 'float'); $.ready(function() { return $.add(d.body, sc); }); @@ -12918,7 +12935,10 @@ this.posts.forEach(function(post) { postCount++; if (post.file) { - return fileCount++; + fileCount++; + } + if (Conf["Page Count in Stats"]) { + return ThreadStats.lastPost = post.info.date; } }); ThreadStats.thread = this; @@ -12977,6 +12997,7 @@ if (!Conf["Page Count in Stats"]) { return; } + clearTimeout(ThreadStats.timeout); if (ThreadStats.thread.isDead) { ThreadStats.pageCountEl.textContent = 'Dead'; $.addClass(ThreadStats.pageCountEl, 'warning'); @@ -13005,6 +13026,7 @@ } ThreadStats.pageCountEl.textContent = page.page; (page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning'); + ThreadStats.lastPageUpdate = new Date(thread.last_modified * $.SECOND); return; } } @@ -13019,15 +13041,18 @@ } if (Conf['Updater and Stats in Header']) { this.dialog = sc = $.el('span', { - innerHTML: "[]\u00A0", id: 'updater' }); + $.extend(sc, { + innerHTML: "[]" + }); $.ready(function() { return Header.addShortcut(sc); }); } else { - this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "
"); - $.addClass(doc, 'float'); + this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', { + innerHTML: "
" + }); $.ready(function() { $.addClass(doc, 'float'); return $.add(d.body, sc); @@ -13044,24 +13069,21 @@ for (name in _ref) { conf = _ref[name]; checked = Conf[name] ? 'checked' : ''; - el = $.el('label', { - title: "" + conf[1], - innerHTML: " " + name - }); + el = UI.checkbox(name, " " + name); input = el.firstElementChild; $.on(input, 'change', $.cb.checked); if (input.name === 'Scroll BG') { $.on(input, 'change', this.cb.scrollBG); this.cb.scrollBG(); } else if (input.name === 'Auto Update') { - $.on(input, 'change', this.cb.update); + $.on(input, 'change', this.cb.autoUpdate); } subEntries.push({ el: el }); } this.settings = $.el('span', { - innerHTML: 'Interval' + innerHTML: "Interval" }); $.on(this.settings, 'click', this.intervalShortcut); subEntries.push({ @@ -13122,6 +13144,7 @@ ThreadUpdater.thread = this; ThreadUpdater.root = this.OP.nodes.root.parentNode; ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1]; + ThreadUpdater.outdateCount = 0; ThreadUpdater.cb.interval.call($.el('input', { value: Conf['Interval'], name: 'Interval' @@ -13134,7 +13157,7 @@ } else { ThreadUpdater.cb.online(); } - Rice.nodes(ThreadUpdater.dialog); + return Rice.nodes(ThreadUpdater.dialog); }, /* @@ -13144,14 +13167,18 @@ beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA', cb: { online: function() { + if (ThreadUpdater.thread.isDead) { + return; + } if (ThreadUpdater.online = navigator.onLine) { ThreadUpdater.outdateCount = 0; ThreadUpdater.setInterval(); - ThreadUpdater.set('status', null, null); + ThreadUpdater.set('status', '', ''); return; } - ThreadUpdater.set('timer', null); - return ThreadUpdater.set('status', 'Offline', 'warning'); + ThreadUpdater.set('timer', ''); + ThreadUpdater.set('status', 'Offline', 'warning'); + return clearTimeout(ThreadUpdater.timeoutID); }, post: function(e) { if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) { @@ -13164,14 +13191,14 @@ }, checkpost: function(e) { if (!ThreadUpdater.checkPostCount) { - if (e.detail.threadID !== ThreadUpdater.thread.ID) { + if (e && e.detail.threadID !== ThreadUpdater.thread.ID) { return; } ThreadUpdater.seconds = 0; ThreadUpdater.outdateCount = 0; ThreadUpdater.set('timer', '...'); } - if (!(g.DEAD || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) { + if (!(ThreadUpdater.thread.isDead || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) { return setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND); } ThreadUpdater.setInterval(); @@ -13195,6 +13222,9 @@ return !d.hidden; }; }, + autoUpdate: function(e) { + return ThreadUpdater.count(ThreadUpdater.isUpdating = this.checked); + }, interval: function(e) { var val; val = parseInt(this.value, 10); @@ -13211,7 +13241,6 @@ req = ThreadUpdater.req; switch (req.status) { case 200: - g.DEAD = false; ThreadUpdater.parse(req.response.posts); if (ThreadUpdater.thread.isArchived) { ThreadUpdater.set('status', 'Archived', 'warning'); @@ -13316,9 +13345,10 @@ var n; ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); if (!(n = --ThreadUpdater.seconds)) { + ThreadUpdater.outdateCount++; return ThreadUpdater.update(); } else if (n <= -60) { - ThreadUpdater.set('status', 'Retrying', null); + ThreadUpdater.set('status', 'Retrying', ''); return ThreadUpdater.update(); } else if (n > 0) { return ThreadUpdater.set('timer', n); @@ -13393,18 +13423,27 @@ ThreadUpdater.thread.posts.forEach(function(post) { var ID; ID = +post.ID; - if (__indexOf.call(index, ID) < 0) { - post.kill(); - } else if (post.isDead) { - post.resurrect(); - } else if (post.file && !post.file.isDead && __indexOf.call(files, ID) < 0) { - post.kill(true); + if (!(post.info.date > Date.now() - 60 * $.SECOND)) { + if (__indexOf.call(index, ID) < 0) { + post.kill(); + } else if (post.isDead) { + post.resurrect(); + } else if (post.file && !(post.file.isDead || __indexOf.call(files, ID) >= 0)) { + post.kill(true); + } } if (ThreadUpdater.postID && ThreadUpdater.postID === ID) { return ThreadUpdater.foundPost = true; } }); sendEvent = function() { + var ipCountEl; + if ((OP.unique_ips != null) && (ipCountEl = $.id('unique-ips'))) { + ipCountEl.textContent = OP.unique_ips; + ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, OP.unique_ips === 1 ? 'is' : 'are'); + ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, OP.unique_ips === 1 ? 'poster' : 'posters'); + } + ThreadUpdater.postIDs = index; return $.event('ThreadUpdate', { 404: false, threadID: ThreadUpdater.thread.fullID, @@ -13417,7 +13456,7 @@ }); }; if (!count) { - ThreadUpdater.set('status', null, null); + ThreadUpdater.set('status', '', ''); ThreadUpdater.outdateCount++; sendEvent(); return; @@ -13464,13 +13503,17 @@ return; } this.db = new DataBoard('watchedThreads', this.refresh, true); - this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "
Thread Watcher \uf107
"); + this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', { + innerHTML: "
\r\rThread Watcher \r\\uf021\r\r\r\\uf107\r
\r
" + }); this.status = $('#watcher-status', this.dialog); this.list = this.dialog.lastElementChild; + this.refreshButton = $('.refresh', this.dialog); $.on(d, 'QRPostSuccessful', this.cb.post); if (g.VIEW === 'thread') { $.on(d, 'ThreadUpdate', this.cb.threadUpdate); } + $.on(this.refreshButton, 'click', this.fetchAllStatus); $.on(d, '4chanXInitFinished', this.ready); switch (g.VIEW) { case 'index': @@ -13485,18 +13528,75 @@ ThreadWatcher.fetchAllStatus(); this.db.save(); } - return Thread.callbacks.push({ + if (Conf['JSON Navigation'] && Conf['Menu'] && g.BOARD.ID !== 'f') { + Menu.menu.addEntry({ + el: $.el('a', { + href: 'javascript:;' + }), + order: 6, + open: function(_arg) { + var thread; + thread = _arg.thread; + if (!(Conf['Index Mode'] === 'catalog' && g.VIEW === 'index')) { + return false; + } + this.el.textContent = ThreadWatcher.isWatched(thread) ? 'Unwatch thread' : 'Watch thread'; + if (this.cb) { + $.off(this.el, 'click', this.cb); + } + this.cb = function() { + $.event('CloseMenu'); + return ThreadWatcher.toggle(thread); + }; + $.on(this.el, 'click', this.cb); + return true; + } + }); + } + Post.callbacks.push({ name: 'Thread Watcher', cb: this.node }); + return CatalogThread.callbacks.push({ + name: 'Thread Watcher', + cb: this.catalogNode + }); + }, + isWatched: function(thread) { + var _ref; + return (_ref = ThreadWatcher.db) != null ? _ref.get({ + boardID: thread.board.ID, + threadID: thread.ID + }) : void 0; }, node: function() { var toggler; - toggler = $.el('img', { - className: 'watch-thread-link' - }); - $.on(toggler, 'click', ThreadWatcher.cb.toggle); - return $.before($('input', this.OP.nodes.post), toggler); + if (this.isReply) { + return; + } + if (this.isClone) { + toggler = $('.watch-thread-link', this.nodes.post); + } else { + toggler = $.el('img', { + className: 'watch-thread-link' + }); + $.before($('input', this.nodes.post), toggler); + } + return $.on(toggler, 'click', ThreadWatcher.cb.toggle); + }, + catalogNode: function() { + if (ThreadWatcher.isWatched(this.thread)) { + $.addClass(this.nodes.root, 'watched'); + } + return $.on(this.nodes.thumb.parentNode, 'click', (function(_this) { + return function(e) { + if (!(e.button === 0 && e.altKey)) { + return; + } + ThreadWatcher.toggle(_this.thread); + return e.preventDefault(); + }; + })(this)); }, ready: function() { var el; @@ -13541,12 +13641,6 @@ } return $.event('CloseMenu'); }, - checkThreads: function() { - if ($.hasClass(this, 'disabled')) { - return; - } - return ThreadWatcher.fetchAllStatus(); - }, pruneDeads: function() { var boardID, data, threadID, _i, _len, _ref, _ref1; if ($.hasClass(this, 'disabled')) { @@ -13568,7 +13662,10 @@ return $.event('CloseMenu'); }, toggle: function() { - return ThreadWatcher.toggle(Get.threadFromNode(this)); + ThreadWatcher.toggle(Get.threadFromNode(this)); + Index.followedThreadID = thread.ID; + ThreadWatcher.toggle(thread); + return delete Index.followedThreadID; }, rm: function() { var boardID, threadID, _ref; @@ -13587,9 +13684,11 @@ } }, onIndexRefresh: function() { - var boardID, data, threadID, _ref; + var boardID, data, db, threadID, _ref; + db = ThreadWatcher.db; boardID = g.BOARD.ID; - _ref = ThreadWatcher.db.data.boards[boardID]; + db.forceSync(); + _ref = db.data.boards[boardID]; for (threadID in _ref) { data = _ref[threadID]; if (!data.isDead && !(threadID in g.BOARD.threads)) { @@ -13628,10 +13727,12 @@ }, fetchAllStatus: function() { var thread, threads, _i, _len; + ThreadWatcher.db.forceSync(); + ThreadWatcher.unreaddb.forceSync(); + QR.db.forceSync(); if (!(threads = ThreadWatcher.getAll()).length) { return; } - ThreadWatcher.status.textContent = '...'; for (_i = 0, _len = threads.length; _i < _len; _i++) { thread = threads[_i]; ThreadWatcher.fetchStatus(thread); @@ -13640,43 +13741,103 @@ fetchStatus: function(_arg) { var boardID, data, fetchCount, threadID; boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data; - if (data.isDead) { + if (data.isDead && !Conf['Show Unread Count']) { return; } fetchCount = ThreadWatcher.fetchCount; + if (fetchCount.fetching === 0) { + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + } fetchCount.fetching++; return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", { onloadend: function() { - var status; + var isDead, lastReadPost, match, postObj, quotingYou, regexp, status, unread, _i, _len, _ref, _ref1; fetchCount.fetched++; if (fetchCount.fetched === fetchCount.fetching) { fetchCount.fetched = 0; fetchCount.fetching = 0; status = ''; + $.rmClass(ThreadWatcher.refreshButton, 'fa-spin'); } else { status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%"; } ThreadWatcher.status.textContent = status; - if (this.status !== 404) { - return; - } - if (Conf['Auto Prune']) { - ThreadWatcher.db["delete"]({ - boardID: boardID, - threadID: threadID - }); - } else { - data.isDead = true; - ThreadWatcher.db.set({ + if (this.status === 200 && this.response) { + isDead = !!this.response.posts[0].archived; + if (isDead && Conf['Auto Prune']) { + ThreadWatcher.db["delete"]({ + boardID: boardID, + threadID: threadID + }); + ThreadWatcher.refresh(); + return; + } + lastReadPost = ThreadWatcher.unreaddb.get({ boardID: boardID, threadID: threadID, - val: data + defaultValue: 0 }); + unread = quotingYou = 0; + _ref = this.response.posts; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + postObj = _ref[_i]; + if (!(postObj.no > lastReadPost)) { + continue; + } + if ((_ref1 = QR.db) != null ? _ref1.get({ + boardID: boardID, + threadID: threadID, + postID: postObj.no + }) : void 0) { + continue; + } + unread++; + if (!(QR.db && postObj.com)) { + continue; + } + regexp = /]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g; + while (match = regexp.exec(postObj.com)) { + if (QR.db.get({ + boardID: match[1] || boardID, + threadID: match[2] || threadID, + postID: match[3] || match[2] || threadID + })) { + quotingYou++; + continue; + } + } + } + if (isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou) { + data.isDead = isDead; + data.unread = unread; + data.quotingYou = quotingYou; + ThreadWatcher.db.set({ + boardID: boardID, + threadID: threadID, + val: data + }); + return ThreadWatcher.refresh(); + } + } else if (this.status === 404) { + if (Conf['Auto Prune']) { + ThreadWatcher.db["delete"]({ + boardID: boardID, + threadID: threadID + }); + } else { + data.isDead = true; + delete data.unread; + delete data.quotingYou; + ThreadWatcher.db.set({ + boardID: boardID, + threadID: threadID, + val: data + }); + } + return ThreadWatcher.refresh(); } - return ThreadWatcher.refresh(); } - }, { - type: 'head' }); }, getAll: function() { @@ -13700,24 +13861,31 @@ return all; }, makeLine: function(boardID, threadID, data) { - var div, fullID, href, link, x; + var count, div, fullID, link, title, x; x = $.el('a', { className: 'fa', href: 'javascript:;', textContent: '\uf00d' }); $.on(x, 'click', ThreadWatcher.cb.rm); - if (data.isDead) { - href = Redirect.to('thread', { - boardID: boardID, - threadID: threadID - }); - } link = $.el('a', { - href: href || ("/" + boardID + "/thread/" + threadID), + href: "/" + boardID + "/thread/" + threadID, textContent: data.excerpt, - title: data.excerpt + title: data.excerpt, + className: 'watcher-link' }); + if (Conf['Show Unread Count'] && (data.unread != null)) { + count = $.el('span', { + textContent: "(" + data.unread + ")", + className: 'watcher-unread' + }); + $.add(link, count); + } + title = $.el('span', { + textContent: data.excerpt, + className: 'watcher-title' + }); + $.add(link, title); div = $.el('div'); fullID = "" + boardID + "." + threadID; div.dataset.fullID = fullID; @@ -13727,11 +13895,19 @@ if (data.isDead) { $.addClass(div, 'dead-thread'); } + if (Conf['Show Unread Count']) { + if (data.unread) { + $.addClass(div, 'replies-unread'); + } + if (data.quotingYou) { + $.addClass(div, 'replies-quoting-you'); + } + } $.add(div, [x, $.tn(' '), link]); return div; }, refresh: function() { - var boardID, data, helper, list, nodes, refresher, thread, threadID, threads, toggler, watched, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3; + var boardID, data, list, nodes, refresher, threadID, _i, _j, _len, _len1, _ref, _ref1, _ref2; nodes = []; _ref = ThreadWatcher.getAll(); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -13741,24 +13917,76 @@ list = ThreadWatcher.list; $.rmAll(list); $.add(list, nodes); - threads = g.BOARD.threads; - _ref2 = threads.keys; + g.threads.forEach(function(thread) { + var helper, post, toggler, _j, _len1, _ref2; + helper = ThreadWatcher.isWatched(thread) ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch']; + if (thread.OP) { + _ref2 = [thread.OP].concat(__slice.call(thread.OP.clones)); + for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { + post = _ref2[_j]; + toggler = $('.watch-thread-link', post.nodes.post); + $[helper[0]](toggler, 'watched'); + toggler.title = "" + helper[1] + " Thread"; + } + } + if (thread.catalogView) { + return $[helper[0]](thread.catalogView.nodes.root, 'watched'); + } + }); + _ref2 = ThreadWatcher.menu.refreshers; for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { - threadID = _ref2[_j]; - thread = threads[threadID]; - toggler = $('.watch-thread-link', thread.OP.nodes.post); - watched = ThreadWatcher.db.get({ - boardID: thread.board.ID, + refresher = _ref2[_j]; + refresher(); + } + if (Index.nodes && Conf['Pin Watched Threads']) { + Index.sort(); + return Index.buildIndex(); + } + }, + update: function(boardID, threadID, newData) { + var data, key, line, n, newLine, val, _ref; + if (!(data = (_ref = ThreadWatcher.db) != null ? _ref.get({ + boardID: boardID, + threadID: threadID + }) : void 0)) { + return; + } + if (newData.isDead && Conf['Auto Prune']) { + ThreadWatcher.db["delete"]({ + boardID: boardID, threadID: threadID }); - helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch']; - $[helper[0]](toggler, 'watched'); - toggler.title = "" + helper[1] + " Thread"; + ThreadWatcher.refresh(); + return; } - _ref3 = ThreadWatcher.menu.refreshers; - for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) { - refresher = _ref3[_k]; - refresher(); + n = 0; + for (key in newData) { + val = newData[key]; + if (data[key] !== val) { + n++; + } + } + if (!n) { + return; + } + ThreadWatcher.db.forceSync(); + if (!(data = ThreadWatcher.db.get({ + boardID: boardID, + threadID: threadID + }))) { + return; + } + $.extend(data, newData); + ThreadWatcher.db.set({ + boardID: boardID, + threadID: threadID, + val: data + }); + if (line = $("#watched-threads > [data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog)) { + newLine = ThreadWatcher.makeLine(boardID, threadID, data); + return $.replace(line, newLine); + } else { + return ThreadWatcher.refresh(); } }, toggle: function(thread) { @@ -13795,7 +14023,14 @@ threadID: threadID, val: data }); - return ThreadWatcher.refresh(); + ThreadWatcher.refresh(); + if (Conf['Show Unread Count']) { + return ThreadWatcher.fetchStatus({ + boardID: boardID, + threadID: threadID, + data: data + }); + } }, rm: function(boardID, threadID) { ThreadWatcher.db["delete"]({ @@ -13825,12 +14060,12 @@ if (!Conf['Thread Watcher']) { return; } - menu = new UI.Menu(); + menu = this.menu = new UI.Menu('thread watcher'); $.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) { return menu.toggle(e, this, ThreadWatcher); }); this.addHeaderMenuEntry(); - return this.addMenuEntries(menu); + return this.addMenuEntries; }, addHeaderMenuEntry: function() { var entryEl; @@ -13855,7 +14090,7 @@ return entryEl.textContent = text; }); }, - addMenuEntries: function(menu) { + addMenuEntries: function() { var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1; entries = []; entries.push({ @@ -13869,22 +14104,11 @@ return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled'); } }); - entries.push({ - cb: ThreadWatcher.cb.checkThreads, - entry: { - el: $.el('a', { - textContent: 'Check 404\'d threads' - }) - }, - refresh: function() { - return ($('div:not(.dead-thread)', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled'); - } - }); entries.push({ cb: ThreadWatcher.cb.pruneDeads, entry: { el: $.el('a', { - textContent: 'Prune 404\'d threads' + textContent: 'Prune dead threads' }) }, refresh: function() { @@ -13916,22 +14140,18 @@ if (refresh) { this.refreshers.push(refresh.bind(entry)); } - menu.addEntry(entry); + this.menu.addEntry(entry); } }, createSubEntry: function(name, desc) { var entry, input; entry = { type: 'thread watcher', - el: $.el('label', { - innerHTML: " " + name, - title: desc - }) + el: UI.checkbox(name, " " + name) }; input = entry.el.firstElementChild; - input.checked = Conf[name]; $.on(input, 'change', $.cb.checked); - if (name === 'Current Board') { + if (name === 'Current Board' || name === 'Show Unread Count') { $.on(input, 'change', ThreadWatcher.refresh); } return entry; @@ -15757,10 +15977,14 @@ } }, getThread: function() { - var threadRoot, _i, _len, _ref; + var thread, threadRoot, _i, _len, _ref; _ref = $$('.thread'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { threadRoot = _ref[_i]; + thread = Get.threadFromRoot(threadRoot); + if (thread.isHidden && !thread.stub) { + continue; + } if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) { return threadRoot; } @@ -15768,7 +15992,10 @@ return $('.board'); }, scroll: function(delta) { - var axis, next, thread, top; + var axis, extra, next, thread, top, _ref; + if ((_ref = d.activeElement) != null) { + _ref.blur(); + } thread = Nav.getThread(); axis = delta === +1 ? 'following' : 'preceding'; if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) { @@ -15777,48 +16004,64 @@ thread = next; } } - return Header.scrollTo(thread); + extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom; + if (extra > 0) { + d.body.style.marginBottom = "" + extra + "px"; + } + Header.scrollTo(thread); + if (extra > 0 && !Nav.haveExtra) { + Nav.haveExtra = true; + return $.on(d, 'scroll', Nav.removeExtra); + } + }, + removeExtra: function() { + var extra; + extra = doc.clientHeight - d.body.getBoundingClientRect().bottom; + if (extra > 0) { + return d.body.style.marginBottom = "" + extra + "px"; + } else { + d.body.style.marginBottom = null; + delete Nav.haveExtra; + return $.off(d, 'scroll', Nav.removeExtra); + } } }; RelativeDates = { INTERVAL: $.MINUTE / 2, init: function() { - switch (g.VIEW) { - case 'index': - this.flush(); - $.on(d, 'visibilitychange', this.flush); - if (!Conf['Relative Post Dates']) { - return; - } - break; - case 'thread': - if (!Conf['Relative Post Dates']) { - return; - } - this.flush(); - $.on(d, 'visibilitychange ThreadUpdate', this.flush); - break; - default: - return; + var _ref; + if (((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Relative Post Dates'] && !Conf['Relative Date Title'] || g.VIEW === 'index' && Conf['JSON Navigation'] && g.BOARD.ID !== 'f') { + this.flush(); + $.on(d, 'visibilitychange ThreadUpdate', this.flush); + } + if (Conf['Relative Post Dates']) { + return Post.callbacks.push({ + name: 'Relative Post Dates', + cb: this.node + }); } - return Post.callbacks.push({ - name: 'Relative Post Dates', - cb: this.node - }); }, node: function() { var dateEl; + dateEl = this.nodes.date; + if (Conf['Relative Date Title']) { + $.on(dateEl, 'mouseover', (function(_this) { + return function() { + return RelativeDates.hover(_this); + }; + })(this)); + return; + } if (this.isClone) { return; } - dateEl = this.nodes.date; dateEl.title = dateEl.textContent; return RelativeDates.update(this); }, relative: function(diff, now, date) { var days, months, number, rounded, unit, years; - unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = (months + 12) % 12) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second'); + unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = months + 12 * years) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second'); rounded = Math.round(number); if (rounded !== 1) { unit += 's'; @@ -15841,6 +16084,13 @@ clearTimeout(RelativeDates.timeout); return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL); }, + hover: function(post) { + var date, diff, now; + date = post.info.date; + now = new Date(); + diff = now - date; + return post.nodes.date.title = RelativeDates.relative(diff, now, date); + }, update: function(data, now) { var date, diff, isPost, relative, singlePost, _i, _len, _ref; isPost = data instanceof Post; @@ -15880,44 +16130,45 @@ if (Conf['Reveal Spoilers']) { $.addClass(doc, 'reveal-spoilers'); } - if (Conf['Remove Spoilers']) { - return $.addClass(doc, 'remove-spoilers'); - } - } - }; - - Report = { - init: function() { - if (!/report/.test(location.search)) { + if (!Conf['Remove Spoilers']) { return; } - return $.asap((function() { - return $.id('recaptcha_response_field'); - }), Report.ready); + $.addClass(doc, 'remove-spoilers'); + Post.callbacks.push({ + name: 'Reveal Spoilers', + cb: this.node + }); + CatalogThread.callbacks.push({ + name: 'Reveal Spoilers', + cb: this.node + }); + if (g.VIEW === 'archive') { + return $.ready(function() { + return RemoveSpoilers.unspoiler($.id('arc-list')); + }); + } }, - ready: function() { - var field; - field = $.id('recaptcha_response_field'); - $.on(field, 'keydown', function(e) { - if (e.keyCode === 8 && !field.value) { - return $.globalEval('Recaptcha.reload("t")'); - } - }); - return $.on($('form'), 'submit', function(e) { - var response; - e.preventDefault(); - response = field.value.trim(); - if (!/\s|^\d+$/.test(response)) { - field.value = "" + response + " " + response; - } - return this.submit(); - }); + node: function(post) { + return RemoveSpoilers.unspoiler(this.nodes.comment); + }, + unspoiler: function(el) { + var span, spoiler, spoilers, _i, _len; + spoilers = $$('s', el); + for (_i = 0, _len = spoilers.length; _i < _len; _i++) { + spoiler = spoilers[_i]; + span = $.el('span', { + className: 'removed-spoiler' + }); + $.replace(spoiler, span); + $.add(span, __slice.call(spoiler.childNodes)); + } } }; Time = { init: function() { - if (!Conf['Time Formatting']) { + var _ref; + if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Time Formatting'])) { return; } return Post.callbacks.push({ @@ -15932,7 +16183,7 @@ return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date); }, format: function(formatString, date) { - return formatString.replace(/%([A-Za-z])/g, function(s, c) { + return formatString.replace(/%(.)/g, function(s, c) { if (c in Time.formatters) { return Time.formatters[c].call(date); } else { @@ -16008,6 +16259,9 @@ }, Y: function() { return this.getFullYear(); + }, + '%': function() { + return '%'; } } }; diff --git a/builds/crx/script.js b/builds/crx/script.js index 078c7a668..52d9adbf1 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -88,7 +88,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, AntiAutoplay, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, Embedding, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, IDHighlight, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -3233,6 +3233,7 @@ this.isPinned = false; this.isSticky = false; this.isClosed = false; + this.isArchived = false; this.postLimit = false; this.fileLimit = false; this.ipCount = void 0; @@ -12769,6 +12770,19 @@ Favicon = { init: function() { + return $.asap((function() { + return d.head && (Favicon.el = $('link[rel="shortcut icon"]', d.head)); + }), Favicon.initAsap); + }, + initAsap: function() { + var href; + Favicon.el.type = 'image/x-icon'; + href = Favicon.el.href; + Favicon.SFW = /ws\.ico$/.test(href); + Favicon["default"] = href; + return Favicon["switch"](); + }, + "switch": function() { var f, funreadDeadY, i, items, t; items = { ferongr: ['iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///9zBQC/AADpDAP/gID/q6voCwJJTwpOAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxUlEQVR42q1TOwrCQBB9s0FRtJI0WoqFtSLYegoP4gVSeJsUHsHSI3iFeIqRXXgwrhlXwYHHhLwPTB7B36abBCV+0pA4DUBQUNZYQptGtW3jtoKyxgoe0yrBCoyZfL/5ioQ3URZOXW9I341l3oo+NXEZiW4CEuIzvPECopED4OaZ3RNmeAm4u+a8Jr5f17VyVoL8fr8qcltzwlyyj2iqcgPOQ9ExkHAITgD75bYBe0A5S4H/P9htuWMF3QXoQpwaKeT+lnsC6JE5I6aq6fEAAAAASUVORK5CYII=', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8AcH4AtswA2PJ55fKi6fIA1/FtpPADAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxElEQVQ4y2NgoBq4/vE/HJOsBiRQUIfA2AzBqQYqUfn00/9FLz+BaQxDCKqBmX7jExijKEDSDJPHrnnbGQhGV4RmOFwdVkNwhQMheYwQxhaIi7b9Z9A3gWAQm2BUoQOgRhgA8o7j1ozLC4LCyAZcx6kZI5qg4kLKqggDFFWxJySsUQVzlb4pwgAJaTRvokcVNgOqOv8zcHBCsL07DgNg8YsczzA5MxtUL+DMD8g0slxI/H8GQ/P/DJKyeKIRpglXZsIiBwBhP5O+VbI/JgAAAABJRU5ErkJggg==', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8oeQBJ3ABV/wHM/7Lu/+ZU/gAqUP3dAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAx0lEQVQ4y2NgoBYI+cfwH4ZJVgMS0KhEYGyG4FQDkzjzf9P/d/+fgWl0QwiqgSkI/c8IxsgKkDXD5LFq9rwDweiK0A2HqcNqCK5wICSPEcLYAtH+AMN/IXMIBrEJRie6OEgjDAC5x3FqxuUFNiEUA67j1IweTTBxBQ1puAG86jgSEraogskJWSBcwCGF5k30qMJmgMFEhv/MXBAs5oLDAFj8IsczTE7UEeECbhU8+QGZRpaTi2b4L2zF8J9TGk80wjThykzY5AAW/2O1C2mIbgAAAABJRU5ErkJggg=='], @@ -12884,7 +12898,7 @@ ThreadExcerpt = { init: function() { - if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) { + if ((g.BOARD.ID !== 'f' && g.BOARD.ID !== 'pol') || g.VIEW !== 'thread' || !Conf['Thread Excerpt'] || Conf['Remove Thread Excerpt']) { return; } return Thread.callbacks.push({ @@ -12919,7 +12933,10 @@ }); Header.addShortcut(sc); } else { - this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', "
" + html + "
"); + this.dialog = sc = UI.dialog('thread-stats', 'bottom: 0px; right: 0px;', { + innerHTML: "
" + statsHTML.innerHTML + "
" + }); + $.addClass(doc, 'float'); $.ready(function() { return $.add(d.body, sc); }); @@ -12940,7 +12957,10 @@ this.posts.forEach(function(post) { postCount++; if (post.file) { - return fileCount++; + fileCount++; + } + if (Conf["Page Count in Stats"]) { + return ThreadStats.lastPost = post.info.date; } }); ThreadStats.thread = this; @@ -12999,6 +13019,7 @@ if (!Conf["Page Count in Stats"]) { return; } + clearTimeout(ThreadStats.timeout); if (ThreadStats.thread.isDead) { ThreadStats.pageCountEl.textContent = 'Dead'; $.addClass(ThreadStats.pageCountEl, 'warning'); @@ -13027,6 +13048,7 @@ } ThreadStats.pageCountEl.textContent = page.page; (page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning'); + ThreadStats.lastPageUpdate = new Date(thread.last_modified * $.SECOND); return; } } @@ -13041,15 +13063,18 @@ } if (Conf['Updater and Stats in Header']) { this.dialog = sc = $.el('span', { - innerHTML: "[]\u00A0", id: 'updater' }); + $.extend(sc, { + innerHTML: "[]" + }); $.ready(function() { return Header.addShortcut(sc); }); } else { - this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', "
"); - $.addClass(doc, 'float'); + this.dialog = sc = UI.dialog('updater', 'bottom: 0px; left: 0px;', { + innerHTML: "
" + }); $.ready(function() { $.addClass(doc, 'float'); return $.add(d.body, sc); @@ -13066,24 +13091,21 @@ for (name in _ref) { conf = _ref[name]; checked = Conf[name] ? 'checked' : ''; - el = $.el('label', { - title: "" + conf[1], - innerHTML: " " + name - }); + el = UI.checkbox(name, " " + name); input = el.firstElementChild; $.on(input, 'change', $.cb.checked); if (input.name === 'Scroll BG') { $.on(input, 'change', this.cb.scrollBG); this.cb.scrollBG(); } else if (input.name === 'Auto Update') { - $.on(input, 'change', this.cb.update); + $.on(input, 'change', this.cb.autoUpdate); } subEntries.push({ el: el }); } this.settings = $.el('span', { - innerHTML: '
Interval' + innerHTML: "Interval" }); $.on(this.settings, 'click', this.intervalShortcut); subEntries.push({ @@ -13144,6 +13166,7 @@ ThreadUpdater.thread = this; ThreadUpdater.root = this.OP.nodes.root.parentNode; ThreadUpdater.lastPost = +this.posts.keys[this.posts.keys.length - 1]; + ThreadUpdater.outdateCount = 0; ThreadUpdater.cb.interval.call($.el('input', { value: Conf['Interval'], name: 'Interval' @@ -13156,7 +13179,7 @@ } else { ThreadUpdater.cb.online(); } - Rice.nodes(ThreadUpdater.dialog); + return Rice.nodes(ThreadUpdater.dialog); }, /* @@ -13166,14 +13189,18 @@ beep: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA', cb: { online: function() { + if (ThreadUpdater.thread.isDead) { + return; + } if (ThreadUpdater.online = navigator.onLine) { ThreadUpdater.outdateCount = 0; ThreadUpdater.setInterval(); - ThreadUpdater.set('status', null, null); + ThreadUpdater.set('status', '', ''); return; } - ThreadUpdater.set('timer', null); - return ThreadUpdater.set('status', 'Offline', 'warning'); + ThreadUpdater.set('timer', ''); + ThreadUpdater.set('status', 'Offline', 'warning'); + return clearTimeout(ThreadUpdater.timeoutID); }, post: function(e) { if (!(ThreadUpdater.isUpdating && e.detail.threadID === ThreadUpdater.thread.ID)) { @@ -13186,14 +13213,14 @@ }, checkpost: function(e) { if (!ThreadUpdater.checkPostCount) { - if (e.detail.threadID !== ThreadUpdater.thread.ID) { + if (e && e.detail.threadID !== ThreadUpdater.thread.ID) { return; } ThreadUpdater.seconds = 0; ThreadUpdater.outdateCount = 0; ThreadUpdater.set('timer', '...'); } - if (!(g.DEAD || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) { + if (!(ThreadUpdater.thread.isDead || ThreadUpdater.foundPost || ThreadUpdater.checkPostCount >= 5)) { return setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND); } ThreadUpdater.setInterval(); @@ -13217,6 +13244,9 @@ return !d.hidden; }; }, + autoUpdate: function(e) { + return ThreadUpdater.count(ThreadUpdater.isUpdating = this.checked); + }, interval: function(e) { var val; val = parseInt(this.value, 10); @@ -13233,7 +13263,6 @@ req = ThreadUpdater.req; switch (req.status) { case 200: - g.DEAD = false; ThreadUpdater.parse(req.response.posts); if (ThreadUpdater.thread.isArchived) { ThreadUpdater.set('status', 'Archived', 'warning'); @@ -13338,9 +13367,10 @@ var n; ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); if (!(n = --ThreadUpdater.seconds)) { + ThreadUpdater.outdateCount++; return ThreadUpdater.update(); } else if (n <= -60) { - ThreadUpdater.set('status', 'Retrying', null); + ThreadUpdater.set('status', 'Retrying', ''); return ThreadUpdater.update(); } else if (n > 0) { return ThreadUpdater.set('timer', n); @@ -13415,18 +13445,27 @@ ThreadUpdater.thread.posts.forEach(function(post) { var ID; ID = +post.ID; - if (__indexOf.call(index, ID) < 0) { - post.kill(); - } else if (post.isDead) { - post.resurrect(); - } else if (post.file && !post.file.isDead && __indexOf.call(files, ID) < 0) { - post.kill(true); + if (!(post.info.date > Date.now() - 60 * $.SECOND)) { + if (__indexOf.call(index, ID) < 0) { + post.kill(); + } else if (post.isDead) { + post.resurrect(); + } else if (post.file && !(post.file.isDead || __indexOf.call(files, ID) >= 0)) { + post.kill(true); + } } if (ThreadUpdater.postID && ThreadUpdater.postID === ID) { return ThreadUpdater.foundPost = true; } }); sendEvent = function() { + var ipCountEl; + if ((OP.unique_ips != null) && (ipCountEl = $.id('unique-ips'))) { + ipCountEl.textContent = OP.unique_ips; + ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, OP.unique_ips === 1 ? 'is' : 'are'); + ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, OP.unique_ips === 1 ? 'poster' : 'posters'); + } + ThreadUpdater.postIDs = index; return $.event('ThreadUpdate', { 404: false, threadID: ThreadUpdater.thread.fullID, @@ -13439,7 +13478,7 @@ }); }; if (!count) { - ThreadUpdater.set('status', null, null); + ThreadUpdater.set('status', '', ''); ThreadUpdater.outdateCount++; sendEvent(); return; @@ -13486,13 +13525,17 @@ return; } this.db = new DataBoard('watchedThreads', this.refresh, true); - this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "
Thread Watcher \uf107
"); + this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', { + innerHTML: "
\r\rThread Watcher \r\\uf021\r\r\r\\uf107\r
\r
" + }); this.status = $('#watcher-status', this.dialog); this.list = this.dialog.lastElementChild; + this.refreshButton = $('.refresh', this.dialog); $.on(d, 'QRPostSuccessful', this.cb.post); if (g.VIEW === 'thread') { $.on(d, 'ThreadUpdate', this.cb.threadUpdate); } + $.on(this.refreshButton, 'click', this.fetchAllStatus); $.on(d, '4chanXInitFinished', this.ready); switch (g.VIEW) { case 'index': @@ -13507,18 +13550,75 @@ ThreadWatcher.fetchAllStatus(); this.db.save(); } - return Thread.callbacks.push({ + if (Conf['JSON Navigation'] && Conf['Menu'] && g.BOARD.ID !== 'f') { + Menu.menu.addEntry({ + el: $.el('a', { + href: 'javascript:;' + }), + order: 6, + open: function(_arg) { + var thread; + thread = _arg.thread; + if (!(Conf['Index Mode'] === 'catalog' && g.VIEW === 'index')) { + return false; + } + this.el.textContent = ThreadWatcher.isWatched(thread) ? 'Unwatch thread' : 'Watch thread'; + if (this.cb) { + $.off(this.el, 'click', this.cb); + } + this.cb = function() { + $.event('CloseMenu'); + return ThreadWatcher.toggle(thread); + }; + $.on(this.el, 'click', this.cb); + return true; + } + }); + } + Post.callbacks.push({ name: 'Thread Watcher', cb: this.node }); + return CatalogThread.callbacks.push({ + name: 'Thread Watcher', + cb: this.catalogNode + }); + }, + isWatched: function(thread) { + var _ref; + return (_ref = ThreadWatcher.db) != null ? _ref.get({ + boardID: thread.board.ID, + threadID: thread.ID + }) : void 0; }, node: function() { var toggler; - toggler = $.el('img', { - className: 'watch-thread-link' - }); - $.on(toggler, 'click', ThreadWatcher.cb.toggle); - return $.before($('input', this.OP.nodes.post), toggler); + if (this.isReply) { + return; + } + if (this.isClone) { + toggler = $('.watch-thread-link', this.nodes.post); + } else { + toggler = $.el('img', { + className: 'watch-thread-link' + }); + $.before($('input', this.nodes.post), toggler); + } + return $.on(toggler, 'click', ThreadWatcher.cb.toggle); + }, + catalogNode: function() { + if (ThreadWatcher.isWatched(this.thread)) { + $.addClass(this.nodes.root, 'watched'); + } + return $.on(this.nodes.thumb.parentNode, 'click', (function(_this) { + return function(e) { + if (!(e.button === 0 && e.altKey)) { + return; + } + ThreadWatcher.toggle(_this.thread); + return e.preventDefault(); + }; + })(this)); }, ready: function() { var el; @@ -13563,12 +13663,6 @@ } return $.event('CloseMenu'); }, - checkThreads: function() { - if ($.hasClass(this, 'disabled')) { - return; - } - return ThreadWatcher.fetchAllStatus(); - }, pruneDeads: function() { var boardID, data, threadID, _i, _len, _ref, _ref1; if ($.hasClass(this, 'disabled')) { @@ -13590,7 +13684,10 @@ return $.event('CloseMenu'); }, toggle: function() { - return ThreadWatcher.toggle(Get.threadFromNode(this)); + ThreadWatcher.toggle(Get.threadFromNode(this)); + Index.followedThreadID = thread.ID; + ThreadWatcher.toggle(thread); + return delete Index.followedThreadID; }, rm: function() { var boardID, threadID, _ref; @@ -13609,9 +13706,11 @@ } }, onIndexRefresh: function() { - var boardID, data, threadID, _ref; + var boardID, data, db, threadID, _ref; + db = ThreadWatcher.db; boardID = g.BOARD.ID; - _ref = ThreadWatcher.db.data.boards[boardID]; + db.forceSync(); + _ref = db.data.boards[boardID]; for (threadID in _ref) { data = _ref[threadID]; if (!data.isDead && !(threadID in g.BOARD.threads)) { @@ -13650,10 +13749,12 @@ }, fetchAllStatus: function() { var thread, threads, _i, _len; + ThreadWatcher.db.forceSync(); + ThreadWatcher.unreaddb.forceSync(); + QR.db.forceSync(); if (!(threads = ThreadWatcher.getAll()).length) { return; } - ThreadWatcher.status.textContent = '...'; for (_i = 0, _len = threads.length; _i < _len; _i++) { thread = threads[_i]; ThreadWatcher.fetchStatus(thread); @@ -13662,43 +13763,103 @@ fetchStatus: function(_arg) { var boardID, data, fetchCount, threadID; boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data; - if (data.isDead) { + if (data.isDead && !Conf['Show Unread Count']) { return; } fetchCount = ThreadWatcher.fetchCount; + if (fetchCount.fetching === 0) { + ThreadWatcher.status.textContent = '...'; + $.addClass(ThreadWatcher.refreshButton, 'fa-spin'); + } fetchCount.fetching++; return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", { onloadend: function() { - var status; + var isDead, lastReadPost, match, postObj, quotingYou, regexp, status, unread, _i, _len, _ref, _ref1; fetchCount.fetched++; if (fetchCount.fetched === fetchCount.fetching) { fetchCount.fetched = 0; fetchCount.fetching = 0; status = ''; + $.rmClass(ThreadWatcher.refreshButton, 'fa-spin'); } else { status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%"; } ThreadWatcher.status.textContent = status; - if (this.status !== 404) { - return; - } - if (Conf['Auto Prune']) { - ThreadWatcher.db["delete"]({ - boardID: boardID, - threadID: threadID - }); - } else { - data.isDead = true; - ThreadWatcher.db.set({ + if (this.status === 200 && this.response) { + isDead = !!this.response.posts[0].archived; + if (isDead && Conf['Auto Prune']) { + ThreadWatcher.db["delete"]({ + boardID: boardID, + threadID: threadID + }); + ThreadWatcher.refresh(); + return; + } + lastReadPost = ThreadWatcher.unreaddb.get({ boardID: boardID, threadID: threadID, - val: data + defaultValue: 0 }); + unread = quotingYou = 0; + _ref = this.response.posts; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + postObj = _ref[_i]; + if (!(postObj.no > lastReadPost)) { + continue; + } + if ((_ref1 = QR.db) != null ? _ref1.get({ + boardID: boardID, + threadID: threadID, + postID: postObj.no + }) : void 0) { + continue; + } + unread++; + if (!(QR.db && postObj.com)) { + continue; + } + regexp = /]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g; + while (match = regexp.exec(postObj.com)) { + if (QR.db.get({ + boardID: match[1] || boardID, + threadID: match[2] || threadID, + postID: match[3] || match[2] || threadID + })) { + quotingYou++; + continue; + } + } + } + if (isDead !== data.isDead || unread !== data.unread || quotingYou !== data.quotingYou) { + data.isDead = isDead; + data.unread = unread; + data.quotingYou = quotingYou; + ThreadWatcher.db.set({ + boardID: boardID, + threadID: threadID, + val: data + }); + return ThreadWatcher.refresh(); + } + } else if (this.status === 404) { + if (Conf['Auto Prune']) { + ThreadWatcher.db["delete"]({ + boardID: boardID, + threadID: threadID + }); + } else { + data.isDead = true; + delete data.unread; + delete data.quotingYou; + ThreadWatcher.db.set({ + boardID: boardID, + threadID: threadID, + val: data + }); + } + return ThreadWatcher.refresh(); } - return ThreadWatcher.refresh(); } - }, { - type: 'head' }); }, getAll: function() { @@ -13722,24 +13883,31 @@ return all; }, makeLine: function(boardID, threadID, data) { - var div, fullID, href, link, x; + var count, div, fullID, link, title, x; x = $.el('a', { className: 'fa', href: 'javascript:;', textContent: '\uf00d' }); $.on(x, 'click', ThreadWatcher.cb.rm); - if (data.isDead) { - href = Redirect.to('thread', { - boardID: boardID, - threadID: threadID - }); - } link = $.el('a', { - href: href || ("/" + boardID + "/thread/" + threadID), + href: "/" + boardID + "/thread/" + threadID, textContent: data.excerpt, - title: data.excerpt + title: data.excerpt, + className: 'watcher-link' }); + if (Conf['Show Unread Count'] && (data.unread != null)) { + count = $.el('span', { + textContent: "(" + data.unread + ")", + className: 'watcher-unread' + }); + $.add(link, count); + } + title = $.el('span', { + textContent: data.excerpt, + className: 'watcher-title' + }); + $.add(link, title); div = $.el('div'); fullID = "" + boardID + "." + threadID; div.dataset.fullID = fullID; @@ -13749,11 +13917,19 @@ if (data.isDead) { $.addClass(div, 'dead-thread'); } + if (Conf['Show Unread Count']) { + if (data.unread) { + $.addClass(div, 'replies-unread'); + } + if (data.quotingYou) { + $.addClass(div, 'replies-quoting-you'); + } + } $.add(div, [x, $.tn(' '), link]); return div; }, refresh: function() { - var boardID, data, helper, list, nodes, refresher, thread, threadID, threads, toggler, watched, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3; + var boardID, data, list, nodes, refresher, threadID, _i, _j, _len, _len1, _ref, _ref1, _ref2; nodes = []; _ref = ThreadWatcher.getAll(); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -13763,24 +13939,76 @@ list = ThreadWatcher.list; $.rmAll(list); $.add(list, nodes); - threads = g.BOARD.threads; - _ref2 = threads.keys; + g.threads.forEach(function(thread) { + var helper, post, toggler, _j, _len1, _ref2; + helper = ThreadWatcher.isWatched(thread) ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch']; + if (thread.OP) { + _ref2 = [thread.OP].concat(__slice.call(thread.OP.clones)); + for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { + post = _ref2[_j]; + toggler = $('.watch-thread-link', post.nodes.post); + $[helper[0]](toggler, 'watched'); + toggler.title = "" + helper[1] + " Thread"; + } + } + if (thread.catalogView) { + return $[helper[0]](thread.catalogView.nodes.root, 'watched'); + } + }); + _ref2 = ThreadWatcher.menu.refreshers; for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { - threadID = _ref2[_j]; - thread = threads[threadID]; - toggler = $('.watch-thread-link', thread.OP.nodes.post); - watched = ThreadWatcher.db.get({ - boardID: thread.board.ID, + refresher = _ref2[_j]; + refresher(); + } + if (Index.nodes && Conf['Pin Watched Threads']) { + Index.sort(); + return Index.buildIndex(); + } + }, + update: function(boardID, threadID, newData) { + var data, key, line, n, newLine, val, _ref; + if (!(data = (_ref = ThreadWatcher.db) != null ? _ref.get({ + boardID: boardID, + threadID: threadID + }) : void 0)) { + return; + } + if (newData.isDead && Conf['Auto Prune']) { + ThreadWatcher.db["delete"]({ + boardID: boardID, threadID: threadID }); - helper = watched ? ['addClass', 'Unwatch'] : ['rmClass', 'Watch']; - $[helper[0]](toggler, 'watched'); - toggler.title = "" + helper[1] + " Thread"; + ThreadWatcher.refresh(); + return; } - _ref3 = ThreadWatcher.menu.refreshers; - for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) { - refresher = _ref3[_k]; - refresher(); + n = 0; + for (key in newData) { + val = newData[key]; + if (data[key] !== val) { + n++; + } + } + if (!n) { + return; + } + ThreadWatcher.db.forceSync(); + if (!(data = ThreadWatcher.db.get({ + boardID: boardID, + threadID: threadID + }))) { + return; + } + $.extend(data, newData); + ThreadWatcher.db.set({ + boardID: boardID, + threadID: threadID, + val: data + }); + if (line = $("#watched-threads > [data-full-i-d='" + boardID + "." + threadID + "']", ThreadWatcher.dialog)) { + newLine = ThreadWatcher.makeLine(boardID, threadID, data); + return $.replace(line, newLine); + } else { + return ThreadWatcher.refresh(); } }, toggle: function(thread) { @@ -13817,7 +14045,14 @@ threadID: threadID, val: data }); - return ThreadWatcher.refresh(); + ThreadWatcher.refresh(); + if (Conf['Show Unread Count']) { + return ThreadWatcher.fetchStatus({ + boardID: boardID, + threadID: threadID, + data: data + }); + } }, rm: function(boardID, threadID) { ThreadWatcher.db["delete"]({ @@ -13847,12 +14082,12 @@ if (!Conf['Thread Watcher']) { return; } - menu = new UI.Menu(); + menu = this.menu = new UI.Menu('thread watcher'); $.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) { return menu.toggle(e, this, ThreadWatcher); }); this.addHeaderMenuEntry(); - return this.addMenuEntries(menu); + return this.addMenuEntries; }, addHeaderMenuEntry: function() { var entryEl; @@ -13877,7 +14112,7 @@ return entryEl.textContent = text; }); }, - addMenuEntries: function(menu) { + addMenuEntries: function() { var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1; entries = []; entries.push({ @@ -13891,22 +14126,11 @@ return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled'); } }); - entries.push({ - cb: ThreadWatcher.cb.checkThreads, - entry: { - el: $.el('a', { - textContent: 'Check 404\'d threads' - }) - }, - refresh: function() { - return ($('div:not(.dead-thread)', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled'); - } - }); entries.push({ cb: ThreadWatcher.cb.pruneDeads, entry: { el: $.el('a', { - textContent: 'Prune 404\'d threads' + textContent: 'Prune dead threads' }) }, refresh: function() { @@ -13938,22 +14162,18 @@ if (refresh) { this.refreshers.push(refresh.bind(entry)); } - menu.addEntry(entry); + this.menu.addEntry(entry); } }, createSubEntry: function(name, desc) { var entry, input; entry = { type: 'thread watcher', - el: $.el('label', { - innerHTML: " " + name, - title: desc - }) + el: UI.checkbox(name, " " + name) }; input = entry.el.firstElementChild; - input.checked = Conf[name]; $.on(input, 'change', $.cb.checked); - if (name === 'Current Board') { + if (name === 'Current Board' || name === 'Show Unread Count') { $.on(input, 'change', ThreadWatcher.refresh); } return entry; @@ -15778,10 +15998,14 @@ } }, getThread: function() { - var threadRoot, _i, _len, _ref; + var thread, threadRoot, _i, _len, _ref; _ref = $$('.thread'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { threadRoot = _ref[_i]; + thread = Get.threadFromRoot(threadRoot); + if (thread.isHidden && !thread.stub) { + continue; + } if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) { return threadRoot; } @@ -15789,7 +16013,10 @@ return $('.board'); }, scroll: function(delta) { - var axis, next, thread, top; + var axis, extra, next, thread, top, _ref; + if ((_ref = d.activeElement) != null) { + _ref.blur(); + } thread = Nav.getThread(); axis = delta === +1 ? 'following' : 'preceding'; if (next = $.x("" + axis + "-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread)) { @@ -15798,48 +16025,64 @@ thread = next; } } - return Header.scrollTo(thread); + extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom; + if (extra > 0) { + d.body.style.marginBottom = "" + extra + "px"; + } + Header.scrollTo(thread); + if (extra > 0 && !Nav.haveExtra) { + Nav.haveExtra = true; + return $.on(d, 'scroll', Nav.removeExtra); + } + }, + removeExtra: function() { + var extra; + extra = doc.clientHeight - d.body.getBoundingClientRect().bottom; + if (extra > 0) { + return d.body.style.marginBottom = "" + extra + "px"; + } else { + d.body.style.marginBottom = null; + delete Nav.haveExtra; + return $.off(d, 'scroll', Nav.removeExtra); + } } }; RelativeDates = { INTERVAL: $.MINUTE / 2, init: function() { - switch (g.VIEW) { - case 'index': - this.flush(); - $.on(d, 'visibilitychange', this.flush); - if (!Conf['Relative Post Dates']) { - return; - } - break; - case 'thread': - if (!Conf['Relative Post Dates']) { - return; - } - this.flush(); - $.on(d, 'visibilitychange ThreadUpdate', this.flush); - break; - default: - return; + var _ref; + if (((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Relative Post Dates'] && !Conf['Relative Date Title'] || g.VIEW === 'index' && Conf['JSON Navigation'] && g.BOARD.ID !== 'f') { + this.flush(); + $.on(d, 'visibilitychange ThreadUpdate', this.flush); + } + if (Conf['Relative Post Dates']) { + return Post.callbacks.push({ + name: 'Relative Post Dates', + cb: this.node + }); } - return Post.callbacks.push({ - name: 'Relative Post Dates', - cb: this.node - }); }, node: function() { var dateEl; + dateEl = this.nodes.date; + if (Conf['Relative Date Title']) { + $.on(dateEl, 'mouseover', (function(_this) { + return function() { + return RelativeDates.hover(_this); + }; + })(this)); + return; + } if (this.isClone) { return; } - dateEl = this.nodes.date; dateEl.title = dateEl.textContent; return RelativeDates.update(this); }, relative: function(diff, now, date) { var days, months, number, rounded, unit, years; - unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = (months + 12) % 12) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second'); + unit = (number = diff / $.DAY) >= 1 ? (years = now.getYear() - date.getYear(), months = now.getMonth() - date.getMonth(), days = now.getDate() - date.getDate(), years > 1 ? (number = years - (months < 0 || months === 0 && days < 0), 'year') : years === 1 && (months > 0 || months === 0 && days >= 0) ? (number = years, 'year') : (months = months + 12 * years) > 1 ? (number = months - (days < 0), 'month') : months === 1 && days >= 0 ? (number = months, 'month') : 'day') : (number = diff / $.HOUR) >= 1 ? 'hour' : (number = diff / $.MINUTE) >= 1 ? 'minute' : (number = Math.max(0, diff) / $.SECOND, 'second'); rounded = Math.round(number); if (rounded !== 1) { unit += 's'; @@ -15862,6 +16105,13 @@ clearTimeout(RelativeDates.timeout); return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL); }, + hover: function(post) { + var date, diff, now; + date = post.info.date; + now = new Date(); + diff = now - date; + return post.nodes.date.title = RelativeDates.relative(diff, now, date); + }, update: function(data, now) { var date, diff, isPost, relative, singlePost, _i, _len, _ref; isPost = data instanceof Post; @@ -15901,44 +16151,45 @@ if (Conf['Reveal Spoilers']) { $.addClass(doc, 'reveal-spoilers'); } - if (Conf['Remove Spoilers']) { - return $.addClass(doc, 'remove-spoilers'); - } - } - }; - - Report = { - init: function() { - if (!/report/.test(location.search)) { + if (!Conf['Remove Spoilers']) { return; } - return $.asap((function() { - return $.id('recaptcha_response_field'); - }), Report.ready); + $.addClass(doc, 'remove-spoilers'); + Post.callbacks.push({ + name: 'Reveal Spoilers', + cb: this.node + }); + CatalogThread.callbacks.push({ + name: 'Reveal Spoilers', + cb: this.node + }); + if (g.VIEW === 'archive') { + return $.ready(function() { + return RemoveSpoilers.unspoiler($.id('arc-list')); + }); + } }, - ready: function() { - var field; - field = $.id('recaptcha_response_field'); - $.on(field, 'keydown', function(e) { - if (e.keyCode === 8 && !field.value) { - return $.globalEval('Recaptcha.reload("t")'); - } - }); - return $.on($('form'), 'submit', function(e) { - var response; - e.preventDefault(); - response = field.value.trim(); - if (!/\s|^\d+$/.test(response)) { - field.value = "" + response + " " + response; - } - return this.submit(); - }); + node: function(post) { + return RemoveSpoilers.unspoiler(this.nodes.comment); + }, + unspoiler: function(el) { + var span, spoiler, spoilers, _i, _len; + spoilers = $$('s', el); + for (_i = 0, _len = spoilers.length; _i < _len; _i++) { + spoiler = spoilers[_i]; + span = $.el('span', { + className: 'removed-spoiler' + }); + $.replace(spoiler, span); + $.add(span, __slice.call(spoiler.childNodes)); + } } }; Time = { init: function() { - if (!Conf['Time Formatting']) { + var _ref; + if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Time Formatting'])) { return; } return Post.callbacks.push({ @@ -15953,7 +16204,7 @@ return this.nodes.date.textContent = Time.format(Conf['time'], this.info.date); }, format: function(formatString, date) { - return formatString.replace(/%([A-Za-z])/g, function(s, c) { + return formatString.replace(/%(.)/g, function(s, c) { if (c in Time.formatters) { return Time.formatters[c].call(date); } else { @@ -16029,6 +16280,9 @@ }, Y: function() { return this.getFullYear(); + }, + '%': function() { + return '%'; } } }; diff --git a/src/General/html/Monitoring/ThreadWatcher.html b/src/General/html/Monitoring/ThreadWatcher.html index 7773abeb1..32741ab1c 100644 --- a/src/General/html/Monitoring/ThreadWatcher.html +++ b/src/General/html/Monitoring/ThreadWatcher.html @@ -1,5 +1,9 @@
- Thread Watcher + + Thread Watcher + \uf021 + + \uf107
-
+
\ No newline at end of file diff --git a/src/General/lib/thread.class b/src/General/lib/thread.class index 08adc1f94..2f795b66e 100755 --- a/src/General/lib/thread.class +++ b/src/General/lib/thread.class @@ -3,16 +3,17 @@ class Thread toString: -> @ID constructor: (@ID, @board) -> - @fullID = "#{@board}.#{@ID}" - @posts = new SimpleDict - @isDead = false - @isHidden = false - @isOnTop = false - @isPinned = false - @isSticky = false - @isClosed = false - @postLimit = false - @fileLimit = false + @fullID = "#{@board}.#{@ID}" + @posts = new SimpleDict + @isDead = false + @isHidden = false + @isOnTop = false + @isPinned = false + @isSticky = false + @isClosed = false + @isArchived = false + @postLimit = false + @fileLimit = false @ipCount = undefined @OP = null diff --git a/src/Miscellaneous/Nav.coffee b/src/Miscellaneous/Nav.coffee index b69778689..9b9a0a978 100755 --- a/src/Miscellaneous/Nav.coffee +++ b/src/Miscellaneous/Nav.coffee @@ -33,11 +33,14 @@ Nav = getThread: -> for threadRoot in $$ '.thread' + thread = Get.threadFromRoot threadRoot + continue if thread.isHidden and !thread.stub if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past return threadRoot return $ '.board' scroll: (delta) -> + d.activeElement?.blur() thread = Nav.getThread() axis = if delta is +1 'following' @@ -49,4 +52,21 @@ Nav = # or we're above the first thread and don't want to skip it. top = Header.getTopOf thread thread = next if delta is +1 and top < 5 or delta is -1 and top > -5 + # Add extra space to the end of the page if necessary so that all threads can be selected by keybinds. + extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom + d.body.style.marginBottom = "#{extra}px" if extra > 0 + Header.scrollTo thread + + if extra > 0 and !Nav.haveExtra + Nav.haveExtra = true + $.on d, 'scroll', Nav.removeExtra + + removeExtra: -> + extra = doc.clientHeight - d.body.getBoundingClientRect().bottom + if extra > 0 + d.body.style.marginBottom = "#{extra}px" + else + d.body.style.marginBottom = null + delete Nav.haveExtra + $.off d, 'scroll', Nav.removeExtra diff --git a/src/Miscellaneous/RelativeDates.coffee b/src/Miscellaneous/RelativeDates.coffee index 76a16d522..960a9ec75 100755 --- a/src/Miscellaneous/RelativeDates.coffee +++ b/src/Miscellaneous/RelativeDates.coffee @@ -1,28 +1,28 @@ RelativeDates = INTERVAL: $.MINUTE / 2 init: -> - switch g.VIEW - when 'index' - @flush() - $.on d, 'visibilitychange', @flush - return unless Conf['Relative Post Dates'] - when 'thread' - return unless Conf['Relative Post Dates'] - @flush() - $.on d, 'visibilitychange ThreadUpdate', @flush - else - return + if ( + g.VIEW in ['index', 'thread'] and Conf['Relative Post Dates'] and !Conf['Relative Date Title'] or + g.VIEW is 'index' and Conf['JSON Navigation'] and g.BOARD.ID isnt 'f' + ) + @flush() + $.on d, 'visibilitychange ThreadUpdate', @flush + + if Conf['Relative Post Dates'] + Post.callbacks.push + name: 'Relative Post Dates' + cb: @node - Post.callbacks.push - name: 'Relative Post Dates' - cb: @node node: -> + dateEl = @nodes.date + if Conf['Relative Date Title'] + $.on dateEl, 'mouseover', => RelativeDates.hover @ + return return if @isClone # Show original absolute time as tooltip so users can still know exact times # Since "Time Formatting" runs its `node` before us, the title tooltip will # pick up the user-formatted time instead of 4chan time when enabled. - dateEl = @nodes.date dateEl.title = dateEl.textContent RelativeDates.update @ @@ -39,7 +39,7 @@ RelativeDates = else if years is 1 and (months > 0 or months is 0 and days >= 0) number = years 'year' - else if (months = (months+12)%12 ) > 1 + else if (months = months + 12*years) > 1 number = months - (days < 0) 'month' else if months is 1 and days >= 0 @@ -82,6 +82,12 @@ RelativeDates = clearTimeout RelativeDates.timeout RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL + hover: (post) -> + date = post.info.date + now = new Date() + diff = now - date + post.nodes.date.title = RelativeDates.relative diff, now, date + # `update()`, when called from `flush()`, updates the elements, # and re-calls `setOwnTimeout()` to re-add `data` to the stale list later. update: (data, now) -> diff --git a/src/Miscellaneous/RemoveSpoilers.coffee b/src/Miscellaneous/RemoveSpoilers.coffee index f83f44e94..339fc621e 100755 --- a/src/Miscellaneous/RemoveSpoilers.coffee +++ b/src/Miscellaneous/RemoveSpoilers.coffee @@ -3,5 +3,28 @@ RemoveSpoilers = if Conf['Reveal Spoilers'] $.addClass doc, 'reveal-spoilers' - if Conf['Remove Spoilers'] - $.addClass doc, 'remove-spoilers' + return unless Conf['Remove Spoilers'] + + $.addClass doc, 'remove-spoilers' + + Post.callbacks.push + name: 'Reveal Spoilers' + cb: @node + + CatalogThread.callbacks.push + name: 'Reveal Spoilers' + cb: @node + + if g.VIEW is 'archive' + $.ready -> RemoveSpoilers.unspoiler $.id 'arc-list' + + node: (post) -> + RemoveSpoilers.unspoiler @nodes.comment + + unspoiler: (el) -> + spoilers = $$ 's', el + for spoiler in spoilers + span = $.el 'span', className: 'removed-spoiler' + $.replace spoiler, span + $.add span, [spoiler.childNodes...] + return \ No newline at end of file diff --git a/src/Miscellaneous/Report.coffee b/src/Miscellaneous/Report.coffee deleted file mode 100755 index 4bf7020f1..000000000 --- a/src/Miscellaneous/Report.coffee +++ /dev/null @@ -1,13 +0,0 @@ -Report = - init: -> - return unless /report/.test(location.search) - $.asap (-> $.id 'recaptcha_response_field'), Report.ready - ready: -> - field = $.id 'recaptcha_response_field' - $.on field, 'keydown', (e) -> - $.globalEval 'Recaptcha.reload("t")' if e.keyCode is 8 and not field.value - $.on $('form'), 'submit', (e) -> - e.preventDefault() - response = field.value.trim() - field.value = "#{response} #{response}" unless /\s|^\d+$/.test response - @submit() diff --git a/src/Miscellaneous/Time.coffee b/src/Miscellaneous/Time.coffee index 3d2d3f2f6..f31ce0d7a 100755 --- a/src/Miscellaneous/Time.coffee +++ b/src/Miscellaneous/Time.coffee @@ -1,19 +1,21 @@ Time = init: -> - return if !Conf['Time Formatting'] + return unless g.VIEW in ['index', 'thread'] and Conf['Time Formatting'] Post.callbacks.push name: 'Time Formatting' cb: @node + node: -> return if @isClone @nodes.date.textContent = Time.format Conf['time'], @info.date format: (formatString, date) -> - formatString.replace /%([A-Za-z])/g, (s, c) -> + formatString.replace /%(.)/g, (s, c) -> if c of Time.formatters Time.formatters[c].call(date) else s + day: [ 'Sunday' 'Monday' @@ -23,6 +25,7 @@ Time = 'Friday' 'Saturday' ] + month: [ 'January' 'February' @@ -37,7 +40,9 @@ Time = 'November' 'December' ] + zeroPad: (n) -> if n < 10 then "0#{n}" else n + formatters: a: -> Time.day[@getDay()][...3] A: -> Time.day[@getDay()] @@ -56,3 +61,4 @@ Time = S: -> Time.zeroPad @getSeconds() y: -> @getFullYear().toString()[2..] Y: -> @getFullYear() + '%': -> '%' diff --git a/src/Monitoring/Favicon.coffee b/src/Monitoring/Favicon.coffee index ff25eab64..28db26bc1 100755 --- a/src/Monitoring/Favicon.coffee +++ b/src/Monitoring/Favicon.coffee @@ -1,5 +1,15 @@ Favicon = init: -> + $.asap (-> d.head and Favicon.el = $ 'link[rel="shortcut icon"]', d.head), Favicon.initAsap + + initAsap: -> + Favicon.el.type = 'image/x-icon' + {href} = Favicon.el + Favicon.SFW = /ws\.ico$/.test href + Favicon.default = href + Favicon.switch() + + switch: -> items = { ferongr: [ '<%= grunt.file.read("src/General/img/favicons/ferongr/unreadDead.png", {encoding: "base64"}) %>' diff --git a/src/Monitoring/ThreadExcerpt.coffee b/src/Monitoring/ThreadExcerpt.coffee index 265ea202c..53bb549eb 100755 --- a/src/Monitoring/ThreadExcerpt.coffee +++ b/src/Monitoring/ThreadExcerpt.coffee @@ -1,6 +1,6 @@ ThreadExcerpt = init: -> - return if g.VIEW isnt 'thread' or !Conf['Thread Excerpt'] + return if (g.BOARD.ID isnt 'f' and g.BOARD.ID isnt 'pol') or g.VIEW isnt 'thread' or !Conf['Thread Excerpt'] or Conf['Remove Thread Excerpt'] Thread.callbacks.push name: 'Thread Excerpt' diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee index 0ec6a8c29..e0f48f15d 100755 --- a/src/Monitoring/ThreadStats.coffee +++ b/src/Monitoring/ThreadStats.coffee @@ -21,16 +21,18 @@ ThreadStats = id: 'thread-stats' title: title Header.addShortcut sc + else @dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;', - "
#{html}
" + <%= html('
&{statsHTML}
') %> + $.addClass doc, 'float' $.ready -> $.add d.body, sc - @postCountEl = $ '#post-count', sc - @ipCountEl = $ '#ip-count', sc - @fileCountEl = $ '#file-count', sc - @pageCountEl = $ '#page-count', sc + @postCountEl = $ '#post-count', sc + @ipCountEl = $ '#ip-count', sc + @fileCountEl = $ '#file-count', sc + @pageCountEl = $ '#page-count', sc Thread.callbacks.push name: 'Thread Stats' @@ -42,6 +44,7 @@ ThreadStats = @posts.forEach (post) -> postCount++ fileCount++ if post.file + ThreadStats.lastPost = post.info.date if Conf["Page Count in Stats"] ThreadStats.thread = @ ThreadStats.fetchPage() ThreadStats.update postCount, fileCount, @ipCount @@ -88,6 +91,7 @@ ThreadStats = fetchPage: -> return if !Conf["Page Count in Stats"] + clearTimeout ThreadStats.timeout if ThreadStats.thread.isDead ThreadStats.pageCountEl.textContent = 'Dead' $.addClass ThreadStats.pageCountEl, 'warning' @@ -102,4 +106,6 @@ ThreadStats = for thread in page.threads when thread.no is ThreadStats.thread.ID ThreadStats.pageCountEl.textContent = page.page (if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning' + # Thread data may be stale (modification date given < time of last post). If so, try again on next thread update. + ThreadStats.lastPageUpdate = new Date thread.last_modified * $.SECOND return diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee index 5c2924c0a..03911d010 100755 --- a/src/Monitoring/ThreadUpdater.coffee +++ b/src/Monitoring/ThreadUpdater.coffee @@ -4,14 +4,13 @@ ThreadUpdater = if Conf['Updater and Stats in Header'] @dialog = sc = $.el 'span', - innerHTML: "[]\u00A0" id: 'updater' + $.extend sc, <%= html('[]') %> $.ready -> Header.addShortcut sc else @dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;', - "
" - $.addClass doc, 'float' + <%= html('
') %> $.ready -> $.addClass doc, 'float' $.add d.body, sc @@ -28,20 +27,18 @@ ThreadUpdater = subEntries = [] for name, conf of Config.updater.checkbox checked = if Conf[name] then 'checked' else '' - el = $.el 'label', - title: "#{conf[1]}" - innerHTML: " #{name}" + el = UI.checkbox name, " #{name}" input = el.firstElementChild $.on input, 'change', $.cb.checked if input.name is 'Scroll BG' $.on input, 'change', @cb.scrollBG @cb.scrollBG() else if input.name is 'Auto Update' - $.on input, 'change', @cb.update + $.on input, 'change', @cb.autoUpdate subEntries.push el: el @settings = $.el 'span', - innerHTML: 'Interval' + <%= html('Interval') %> $.on @settings, 'click', @intervalShortcut @@ -92,9 +89,10 @@ ThreadUpdater = Thread.callbacks.disconnect 'Thread Updater' node: -> - ThreadUpdater.thread = @ - ThreadUpdater.root = @OP.nodes.root.parentNode - ThreadUpdater.lastPost = +@posts.keys[@posts.keys.length - 1] + ThreadUpdater.thread = @ + ThreadUpdater.root = @OP.nodes.root.parentNode + ThreadUpdater.lastPost = +@posts.keys[@posts.keys.length - 1] + ThreadUpdater.outdateCount = 0 ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval'] @@ -110,8 +108,6 @@ ThreadUpdater = ThreadUpdater.cb.online() Rice.nodes ThreadUpdater.dialog - - return ### http://freesound.org/people/pierrecartoons1979/sounds/90112/ @@ -121,24 +117,26 @@ ThreadUpdater = cb: online: -> + return if ThreadUpdater.thread.isDead if ThreadUpdater.online = navigator.onLine ThreadUpdater.outdateCount = 0 ThreadUpdater.setInterval() - ThreadUpdater.set 'status', null, null + ThreadUpdater.set 'status', '', '' return - ThreadUpdater.set 'timer', null + ThreadUpdater.set 'timer', '' ThreadUpdater.set 'status', 'Offline', 'warning' + clearTimeout ThreadUpdater.timeoutID post: (e) -> return unless ThreadUpdater.isUpdating and e.detail.threadID is ThreadUpdater.thread.ID ThreadUpdater.outdateCount = 0 setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2 checkpost: (e) -> unless ThreadUpdater.checkPostCount - return unless e.detail.threadID is ThreadUpdater.thread.ID + return if e and e.detail.threadID isnt ThreadUpdater.thread.ID ThreadUpdater.seconds = 0 ThreadUpdater.outdateCount = 0 ThreadUpdater.set 'timer', '...' - unless g.DEAD or ThreadUpdater.foundPost or ThreadUpdater.checkPostCount >= 5 + unless ThreadUpdater.thread.isDead or ThreadUpdater.foundPost or ThreadUpdater.checkPostCount >= 5 return setTimeout ThreadUpdater.update, ++ThreadUpdater.checkPostCount * $.SECOND ThreadUpdater.setInterval() ThreadUpdater.checkPostCount = 0 @@ -155,6 +153,8 @@ ThreadUpdater = -> true else -> not d.hidden + autoUpdate: (e) -> + ThreadUpdater.count ThreadUpdater.isUpdating = @checked interval: (e) -> val = parseInt @value, 10 if val < 1 then val = 1 @@ -164,7 +164,6 @@ ThreadUpdater = {req} = ThreadUpdater switch req.status when 200 - g.DEAD = false ThreadUpdater.parse req.response.posts if ThreadUpdater.thread.isArchived ThreadUpdater.set 'status', 'Archived', 'warning' @@ -257,9 +256,10 @@ ThreadUpdater = timeout: -> ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 unless n = --ThreadUpdater.seconds + ThreadUpdater.outdateCount++ ThreadUpdater.update() else if n <= -60 - ThreadUpdater.set 'status', 'Retrying', null + ThreadUpdater.set 'status', 'Retrying', '' ThreadUpdater.update() else if n > 0 ThreadUpdater.set 'timer', n @@ -307,7 +307,7 @@ ThreadUpdater = ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed ThreadUpdater.thread.postLimit = !!OP.bumplimit ThreadUpdater.thread.fileLimit = !!OP.imagelimit - ThreadUpdater.thread.ipCount = OP.unique_ips if OP.unique_ips? + ThreadUpdater.thread.ipCount = OP.unique_ips if OP.unique_ips? posts = [] # post objects index = [] # existing posts @@ -331,18 +331,28 @@ ThreadUpdater = # continue if post.isDead ID = +post.ID - unless ID in index - post.kill() - else if post.isDead - post.resurrect() - else if post.file and !post.file.isDead and ID not in files - post.kill true + # Assume deleted posts less than 60 seconds old are false positives. + unless post.info.date > Date.now() - 60 * $.SECOND + unless ID in index + post.kill() + else if post.isDead + post.resurrect() + else if post.file and not (post.file.isDead or ID in files) + post.kill true # Fetching your own posts after posting if ThreadUpdater.postID and ThreadUpdater.postID is ID ThreadUpdater.foundPost = true sendEvent = -> + # Update IP count in original post form. + if OP.unique_ips? and ipCountEl = $.id('unique-ips') + ipCountEl.textContent = OP.unique_ips + ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, if OP.unique_ips is 1 then 'is' else 'are') + ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, if OP.unique_ips is 1 then 'poster' else 'posters') + + ThreadUpdater.postIDs = index + $.event 'ThreadUpdate', 404: false threadID: ThreadUpdater.thread.fullID @@ -352,7 +362,7 @@ ThreadUpdater = ipCount: OP.unique_ips unless count - ThreadUpdater.set 'status', null, null + ThreadUpdater.set 'status', '', '' ThreadUpdater.outdateCount++ sendEvent() return diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee index b0c534675..da3e10425 100755 --- a/src/Monitoring/ThreadWatcher.coffee +++ b/src/Monitoring/ThreadWatcher.coffee @@ -3,17 +3,18 @@ ThreadWatcher = return if !Conf['Thread Watcher'] @db = new DataBoard 'watchedThreads', @refresh, true - @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """<%= - grunt.file.read('src/General/html/Monitoring/ThreadWatcher.html').replace(/>\s+<').trim() - %>""" + @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= importHTML('Monitoring/ThreadWatcher') %> @status = $ '#watcher-status', @dialog @list = @dialog.lastElementChild + @refreshButton = $ '.refresh', @dialog $.on d, 'QRPostSuccessful', @cb.post $.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread' + $.on @refreshButton, 'click', @fetchAllStatus $.on d, '4chanXInitFinished', @ready + switch g.VIEW when 'index' $.on d, 'IndexRefresh', @cb.onIndexRefresh @@ -26,15 +27,50 @@ ThreadWatcher = ThreadWatcher.fetchAllStatus() @db.save() - Thread.callbacks.push + if Conf['JSON Navigation'] and Conf['Menu'] and g.BOARD.ID isnt 'f' + Menu.menu.addEntry + el: $.el 'a', href: 'javascript:;' + order: 6 + open: ({thread}) -> + return false unless Conf['Index Mode'] is 'catalog' and g.VIEW is 'index' + @el.textContent = if ThreadWatcher.isWatched thread + 'Unwatch thread' + else + 'Watch thread' + $.off @el, 'click', @cb if @cb + @cb = -> + $.event 'CloseMenu' + ThreadWatcher.toggle thread + $.on @el, 'click', @cb + true + + Post.callbacks.push name: 'Thread Watcher' cb: @node + CatalogThread.callbacks.push + name: 'Thread Watcher' + cb: @catalogNode + + isWatched: (thread) -> + ThreadWatcher.db?.get {boardID: thread.board.ID, threadID: thread.ID} + node: -> - toggler = $.el 'img', - className: 'watch-thread-link' + return if @isReply + if @isClone + toggler = $ '.watch-thread-link', @nodes.post + else + toggler = $.el 'img', + className: 'watch-thread-link' + $.before $('input', @nodes.post), toggler $.on toggler, 'click', ThreadWatcher.cb.toggle - $.before $('input', @OP.nodes.post), toggler + + catalogNode: -> + $.addClass @nodes.root, 'watched' if ThreadWatcher.isWatched @thread + $.on @nodes.thumb.parentNode, 'click', (e) => + return unless e.button is 0 and e.altKey + ThreadWatcher.toggle @thread + e.preventDefault() ready: -> $.off d, '4chanXInitFinished', ThreadWatcher.ready @@ -63,9 +99,6 @@ ThreadWatcher = for a in $$ 'a[title]', ThreadWatcher.list $.open a.href $.event 'CloseMenu' - checkThreads: -> - return if $.hasClass @, 'disabled' - ThreadWatcher.fetchAllStatus() pruneDeads: -> return if $.hasClass @, 'disabled' for {boardID, threadID, data} in ThreadWatcher.getAll() when data.isDead @@ -76,6 +109,9 @@ ThreadWatcher = $.event 'CloseMenu' toggle: -> ThreadWatcher.toggle Get.threadFromNode @ + Index.followedThreadID = thread.ID + ThreadWatcher.toggle thread + delete Index.followedThreadID rm: -> [boardID, threadID] = @parentNode.dataset.fullID.split '.' ThreadWatcher.rm boardID, +threadID @@ -87,8 +123,10 @@ ThreadWatcher = else if Conf['Auto Watch Reply'] ThreadWatcher.add g.threads[boardID + '.' + threadID] onIndexRefresh: -> + {db} = ThreadWatcher boardID = g.BOARD.ID - for threadID, data of ThreadWatcher.db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads + db.forceSync() + for threadID, data of db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads if Conf['Auto Prune'] ThreadWatcher.db.delete {boardID, threadID} else @@ -98,21 +136,26 @@ ThreadWatcher = onThreadRefresh: (e) -> thread = g.threads[e.detail.threadID] return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID} - # Update 404 status. + # Update dead status. ThreadWatcher.add thread fetchCount: fetched: 0 fetching: 0 fetchAllStatus: -> + ThreadWatcher.db.forceSync() + ThreadWatcher.unreaddb.forceSync() + QR.db.forceSync() return unless (threads = ThreadWatcher.getAll()).length - ThreadWatcher.status.textContent = '...' for thread in threads ThreadWatcher.fetchStatus thread return fetchStatus: ({boardID, threadID, data}) -> - return if data.isDead + return if data.isDead and !Conf['Show Unread Count'] {fetchCount} = ThreadWatcher + if fetchCount.fetching is 0 + ThreadWatcher.status.textContent = '...' + $.addClass ThreadWatcher.refreshButton, 'fa-spin' fetchCount.fetching++ $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json", onloadend: -> @@ -121,18 +164,56 @@ ThreadWatcher = fetchCount.fetched = 0 fetchCount.fetching = 0 status = '' + $.rmClass ThreadWatcher.refreshButton, 'fa-spin' else status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%" ThreadWatcher.status.textContent = status - return if @status isnt 404 - if Conf['Auto Prune'] - ThreadWatcher.db.delete {boardID, threadID} - else - data.isDead = true - ThreadWatcher.db.set {boardID, threadID, val: data} - ThreadWatcher.refresh() - , - type: 'head' + + if @status is 200 and @response + isDead = !!@response.posts[0].archived + if isDead and Conf['Auto Prune'] + ThreadWatcher.db.delete {boardID, threadID} + ThreadWatcher.refresh() + return + + lastReadPost = ThreadWatcher.unreaddb.get + boardID: boardID + threadID: threadID + defaultValue: 0 + + unread = quotingYou = 0 + + for postObj in @response.posts + continue unless postObj.no > lastReadPost + continue if QR.db?.get {boardID, threadID, postID: postObj.no} + unread++ + continue unless QR.db and postObj.com + regexp = /]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g + while match = regexp.exec postObj.com + if QR.db.get { + boardID: match[1] or boardID + threadID: match[2] or threadID + postID: match[3] or match[2] or threadID + } + quotingYou++ + continue + + if isDead isnt data.isDead or unread isnt data.unread or quotingYou isnt data.quotingYou + data.isDead = isDead + data.unread = unread + data.quotingYou = quotingYou + ThreadWatcher.db.set {boardID, threadID, val: data} + ThreadWatcher.refresh() + + else if @status is 404 + if Conf['Auto Prune'] + ThreadWatcher.db.delete {boardID, threadID} + else + data.isDead = true + delete data.unread + delete data.quotingYou + ThreadWatcher.db.set {boardID, threadID, val: data} + ThreadWatcher.refresh() getAll: -> all = [] @@ -150,18 +231,31 @@ ThreadWatcher = textContent: '\uf00d' $.on x, 'click', ThreadWatcher.cb.rm - if data.isDead - href = Redirect.to 'thread', {boardID, threadID} link = $.el 'a', - href: href or "/#{boardID}/thread/#{threadID}" + href: "/#{boardID}/thread/#{threadID}" textContent: data.excerpt title: data.excerpt + className: 'watcher-link' + + if Conf['Show Unread Count'] and data.unread? + count = $.el 'span', + textContent: "(#{data.unread})" + className: 'watcher-unread' + $.add link, count + + title = $.el 'span', + textContent: data.excerpt + className: 'watcher-title' + $.add link, title div = $.el 'div' fullID = "#{boardID}.#{threadID}" div.dataset.fullID = fullID - $.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}" - $.addClass div, 'dead-thread' if data.isDead + $.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}" + $.addClass div, 'dead-thread' if data.isDead + if Conf['Show Unread Count'] + $.addClass div, 'replies-unread' if data.unread + $.addClass div, 'replies-quoting-you' if data.quotingYou $.add div, [x, $.tn(' '), link] div refresh: -> @@ -173,18 +267,40 @@ ThreadWatcher = $.rmAll list $.add list, nodes - {threads} = g.BOARD - for threadID in threads.keys - thread = threads[threadID] - toggler = $ '.watch-thread-link', thread.OP.nodes.post - watched = ThreadWatcher.db.get {boardID: thread.board.ID, threadID} - helper = if watched then ['addClass', 'Unwatch'] else ['rmClass', 'Watch'] - $[helper[0]] toggler, 'watched' - toggler.title = "#{helper[1]} Thread" + g.threads.forEach (thread) -> + helper = if ThreadWatcher.isWatched thread then ['addClass', 'Unwatch'] else ['rmClass', 'Watch'] + if thread.OP + for post in [thread.OP, thread.OP.clones...] + toggler = $ '.watch-thread-link', post.nodes.post + $[helper[0]] toggler, 'watched' + toggler.title = "#{helper[1]} Thread" + $[helper[0]] thread.catalogView.nodes.root, 'watched' if thread.catalogView for refresher in ThreadWatcher.menu.refreshers refresher() - return + + if Index.nodes and Conf['Pin Watched Threads'] + Index.sort() + Index.buildIndex() + + update: (boardID, threadID, newData) -> + return unless data = ThreadWatcher.db?.get {boardID, threadID} + if newData.isDead and Conf['Auto Prune'] + ThreadWatcher.db.delete {boardID, threadID} + ThreadWatcher.refresh() + return + n = 0 + n++ for key, val of newData when data[key] isnt val + return unless n + ThreadWatcher.db.forceSync() + return unless data = ThreadWatcher.db.get {boardID, threadID} + $.extend data, newData + ThreadWatcher.db.set {boardID, threadID, val: data} + if line = $ "#watched-threads > [data-full-i-d='#{boardID}.#{threadID}']", ThreadWatcher.dialog + newLine = ThreadWatcher.makeLine boardID, threadID, data + $.replace line, newLine + else + ThreadWatcher.refresh() toggle: (thread) -> boardID = thread.board.ID @@ -205,6 +321,8 @@ ThreadWatcher = data.excerpt = Get.threadExcerpt thread ThreadWatcher.db.set {boardID, threadID, val: data} ThreadWatcher.refresh() + if Conf['Show Unread Count'] + ThreadWatcher.fetchStatus {boardID, threadID, data} rm: (boardID, threadID) -> ThreadWatcher.db.delete {boardID, threadID} ThreadWatcher.refresh() @@ -220,11 +338,11 @@ ThreadWatcher = refreshers: [] init: -> return if !Conf['Thread Watcher'] - menu = new UI.Menu() + menu = @menu = new UI.Menu 'thread watcher' $.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) -> menu.toggle e, @, ThreadWatcher @addHeaderMenuEntry() - @addMenuEntries menu + @addMenuEntries addHeaderMenuEntry: -> return if g.VIEW isnt 'thread' @@ -243,7 +361,7 @@ ThreadWatcher = $.rmClass entryEl, rmClass entryEl.textContent = text - addMenuEntries: (menu) -> + addMenuEntries: -> entries = [] # `Open all` entry @@ -254,20 +372,12 @@ ThreadWatcher = textContent: 'Open all threads' refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled' - # `Check 404'd threads` entry - entries.push - cb: ThreadWatcher.cb.checkThreads - entry: - el: $.el 'a', - textContent: 'Check 404\'d threads' - refresh: -> (if $('div:not(.dead-thread)', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled' - - # `Prune 404'd threads` entry + # `Prune dead threads` entry entries.push cb: ThreadWatcher.cb.pruneDeads entry: el: $.el 'a', - textContent: 'Prune 404\'d threads' + textContent: 'Prune dead threads' refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled' # `Settings` entries: @@ -284,16 +394,14 @@ ThreadWatcher = entry.el.href = 'javascript:;' if entry.el.nodeName is 'A' $.on entry.el, 'click', cb if cb @refreshers.push refresh.bind entry if refresh - menu.addEntry entry + @menu.addEntry entry return + createSubEntry: (name, desc) -> entry = type: 'thread watcher' - el: $.el 'label', - innerHTML: " #{name}" - title: desc + el: UI.checkbox name, " #{name}" input = entry.el.firstElementChild - input.checked = Conf[name] $.on input, 'change', $.cb.checked - $.on input, 'change', ThreadWatcher.refresh if name is 'Current Board' + $.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Unread Count'] entry