diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5de52ae11..0d6af5acc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,18 @@
**MayhemYDG**:
- **New feature**: `Show Dice Roll` (with @carboncopy)
- Shows dice that were entered into the email field on /tg/.
+- **Thread Watcher** improvements:
+ - It is now possible to open all watched threads via the `Open all threads` button in the Thread Watcher's menu.
+ - Added the `Current Board` setting to switch between showing watched threads from the current board or all boards, disabled by default.
+ - About dead (404'd) threads:
+ - Dead threads will be typographically indicated with a strikethrough.
+ - Dead threads will directly link to the corresponding archive when available.
+ - A button to prune all 404'd threads from the list is now available.
+ - Added the `Auto Prune` setting to automatically prune 404'd threads, disabled by default.
+ - The current thread is now highlighted in the list of watched threads.
+ - Watching the current thread can be done in the Header's menu too.
+- Removed the `Check for Updates` setting:
+ - Your browser/userscript manager should handle updates itself automatically.
- Fix impossibility to create new threads when in dead threads.
- Fix flag filtering on /sp/ and /int/.
- Update archives. (with @woxxy and @proplex)
diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js
index b066cc32c..7851634ea 100644
--- a/builds/4chan-X.user.js
+++ b/builds/4chan-X.user.js
@@ -134,7 +134,6 @@
'Index Navigation': [false, 'Add buttons to navigate between threads.'],
'Reply Navigation': [false, 'Add buttons to navigate to top / bottom of thread.'],
'Show Dice Roll': [true, 'Show dice that were entered into the email field.'],
- 'Check for Updates': [true, 'Check for updated versions of 4chan X.'],
'Show Updated Notifications': [true, 'Show notifications when 4chan X is successfully updated.'],
'Emoji': [false, 'Adds icons next to names for different emails'],
'Color User IDs': [false, 'Assign unique colors to user IDs on boards that use them'],
@@ -188,9 +187,7 @@
'Page Count in Stats': [false, 'Display the page count in the thread stats as well.'],
'Updater and Stats in Header': [true, 'Places the thread updater and thread stats in the header instead of floating them.'],
'Thread Watcher': [true, 'Bookmark threads.'],
- 'Toggleable Thread Watcher': [true, 'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.'],
- 'Auto Watch': [true, 'Automatically watch threads you start.'],
- 'Auto Watch Reply': [false, 'Automatically watch threads you reply to.']
+ 'Toggleable Thread Watcher': [true, 'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.']
},
'Posting': {
'Quick Reply': [true, 'All-in-one form to reply, create threads, automate dumping and more.'],
@@ -231,6 +228,12 @@
'Expand from here': [false, 'Expand all images only from current position to thread end.'],
'Advance on contract': [false, 'Advance to next post when contracting an expanded image.']
},
+ threadWatcher: {
+ 'Current Board': [false, 'Only show watched threads from the current board.'],
+ 'Auto Watch': [true, 'Automatically watch threads you start.'],
+ 'Auto Watch Reply': [false, 'Automatically watch threads you reply to.'],
+ 'Auto Prune': [false, 'Automatically prune 404\'d threads.']
+ },
filter: {
name: "# Filter any namefags:\n#/^(?!Anonymous$)/",
uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/",
@@ -423,26 +426,32 @@
}
};
- $.ajax = function(url, options, extra) {
- var form, headers, key, r, sync, type, upCallbacks, val;
+ $.ajax = (function() {
+ var lastModified;
- if (extra == null) {
- extra = {};
- }
- type = extra.type, headers = extra.headers, upCallbacks = extra.upCallbacks, form = extra.form, sync = extra.sync;
- r = new XMLHttpRequest();
- r.overrideMimeType('text/html');
- type || (type = form && 'post' || 'get');
- r.open(type, url, !sync);
- for (key in headers) {
- val = headers[key];
- r.setRequestHeader(key, val);
- }
- $.extend(r, options);
- $.extend(r.upload, upCallbacks);
- r.send(form);
- return r;
- };
+ lastModified = {};
+ return function(url, options, extra) {
+ var form, r, sync, type, upCallbacks, whenModified;
+
+ if (extra == null) {
+ extra = {};
+ }
+ type = extra.type, whenModified = extra.whenModified, upCallbacks = extra.upCallbacks, form = extra.form, sync = extra.sync;
+ r = new XMLHttpRequest();
+ type || (type = form && 'post' || 'get');
+ r.open(type, url, !sync);
+ if (whenModified) {
+ r.setRequestHeader('If-Modified-Since', lastModified[url] || '0');
+ $.on(r, 'load', function() {
+ return lastModified[url] = r.getResponseHeader('Last-Modified');
+ });
+ }
+ $.extend(r, options);
+ $.extend(r.upload, upCallbacks);
+ r.send(form);
+ return r;
+ };
+ })();
$.cache = (function() {
var reqs;
@@ -1198,17 +1207,19 @@
})(Post);
- DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts'];
+ DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads'];
DataBoard = (function() {
- function DataBoard(key, sync) {
+ function DataBoard(key, sync, dontClean) {
var init,
_this = this;
this.key = key;
this.data = Conf[key];
$.sync(key, this.onSync.bind(this));
- this.clean();
+ if (!dontClean) {
+ this.clean();
+ }
if (!sync) {
return;
}
@@ -1219,6 +1230,10 @@
$.on(d, '4chanXInitFinished', init);
}
+ DataBoard.prototype.save = function() {
+ return $.set(this.key, this.data);
+ };
+
DataBoard.prototype["delete"] = function(_arg) {
var boardID, postID, threadID;
@@ -1237,7 +1252,7 @@
} else {
delete this.data.boards[boardID];
}
- return $.set(this.key, this.data);
+ return this.save();
};
DataBoard.prototype.deleteIfEmpty = function(_arg) {
@@ -1267,7 +1282,7 @@
} else {
this.data.boards[boardID] = val;
}
- return $.set(this.key, this.data);
+ return this.save();
};
DataBoard.prototype.get = function(_arg) {
@@ -1311,7 +1326,7 @@
this.ajaxClean(boardID);
}
}
- return $.set(this.key, this.data);
+ return this.save();
};
DataBoard.prototype.ajaxClean = function(boardID) {
@@ -1341,7 +1356,7 @@
boardID: boardID
});
}
- return $.set(_this.key, _this.data);
+ return _this.save();
});
};
@@ -1402,8 +1417,26 @@
Polyfill = {
init: function() {
+ Polyfill.toBlob();
return Polyfill.visibility();
},
+ toBlob: function() {
+ var _base;
+
+ return (_base = HTMLCanvasElement.prototype).toBlob || (_base.toBlob = function(cb) {
+ var data, i, l, ui8a, _i;
+
+ data = atob(this.toDataURL().slice(22));
+ l = data.length;
+ ui8a = new Uint8Array(l);
+ for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) {
+ ui8a[i] = data.charCodeAt(i);
+ }
+ return cb(new Blob([ui8a], {
+ type: 'image/png'
+ }));
+ });
+ },
visibility: function() {
if (!('webkitHidden' in document)) {
return;
@@ -1822,7 +1855,7 @@
date: data.now,
dateUTC: data.time,
comment: data.com,
- capReps: data.capcode_replies,
+ capcodeReplies: data.capcode_replies,
isSticky: !!data.sticky,
isClosed: !!data.closed
};
@@ -1850,9 +1883,9 @@
@license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
*/
- var a, array, boardID, capReps, capcode, capcodeClass, capcodeReplies, capcodeStart, capcodeType, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, generateCapcodeReplies, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref;
+ var a, boardID, capcode, capcodeClass, capcodeReplies, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref;
- postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, capReps = o.capReps, file = o.file;
+ postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, capcodeReplies = o.capcodeReplies, file = o.file;
isOP = postID === threadID;
staticPath = '//static.4chan.org/image/';
if (email) {
@@ -1926,28 +1959,6 @@
tripcode = tripcode ? " " + tripcode + "" : '';
sticky = isSticky ? "
" : '';
closed = isClosed ? "
" : '';
- capcodeReplies = '';
- if (capReps) {
- generateCapcodeReplies = function(capcodeType, array) {
- return "" + ((function() {
- switch (capcodeType) {
- case 'admin':
- return 'Administrator';
- case 'mod':
- return 'Moderator';
- case 'developer':
- return 'Developer';
- }
- })()) + " Repl" + (array.length > 1 ? 'ies' : 'y') + ": " + (array.map(function(ID) {
- return ">>" + ID + "";
- }).join(' ')) + "
";
- };
- for (capcodeType in capReps) {
- array = capReps[capcodeType];
- capcodeReplies += generateCapcodeReplies(capcodeType, array);
- }
- capcodeReplies = "
" + capcodeReplies + "";
- }
container = $.el('div', {
id: "pc" + postID,
className: "postContainer " + (isOP ? 'op' : 'reply') + "Container",
@@ -1962,7 +1973,47 @@
}
quote.href = "/" + boardID + "/res/" + href;
}
+ Build.capcodeReplies({
+ boardID: boardID,
+ threadID: threadID,
+ root: container,
+ capcodeReplies: capcodeReplies
+ });
return container;
+ },
+ capcodeReplies: function(_arg) {
+ var array, boardID, bq, capcodeReplies, capcodeType, generateCapcodeReplies, html, root, threadID;
+
+ boardID = _arg.boardID, threadID = _arg.threadID, bq = _arg.bq, root = _arg.root, capcodeReplies = _arg.capcodeReplies;
+ if (!capcodeReplies) {
+ return;
+ }
+ generateCapcodeReplies = function(capcodeType, array) {
+ return "" + ((function() {
+ switch (capcodeType) {
+ case 'admin':
+ return 'Administrator';
+ case 'mod':
+ return 'Moderator';
+ case 'developer':
+ return 'Developer';
+ }
+ })()) + " Repl" + (array.length > 1 ? 'ies' : 'y') + ": " + (array.map(function(ID) {
+ return ">>" + ID + "";
+ }).join(' ')) + "
";
+ };
+ html = [];
+ for (capcodeType in capcodeReplies) {
+ array = capcodeReplies[capcodeType];
+ html.push(generateCapcodeReplies(capcodeType, array));
+ }
+ bq || (bq = $('blockquote', root));
+ return $.add(bq, [
+ $.el('br'), $.el('br'), $.el('span', {
+ className: 'capcodeReplies',
+ innerHTML: html.join('')
+ })
+ ]);
}
};
@@ -5607,7 +5658,7 @@
img = $.el('img');
img.onload = function() {
- var applyBlob, cv, data, height, i, l, s, ui8a, width, _i;
+ var cv, height, s, width;
s = 90 * 2;
if (_this.file.type === 'image/gif') {
@@ -5631,23 +5682,10 @@
cv.width = img.width = width;
cv.getContext('2d').drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(fileURL);
- applyBlob = function(blob) {
+ return cv.toBlob(function(blob) {
_this.URL = URL.createObjectURL(blob);
return _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
- };
- if (cv.toBlob) {
- cv.toBlob(applyBlob);
- return;
- }
- data = atob(cv.toDataURL().split(',')[1]);
- l = data.length;
- ui8a = new Uint8Array(l);
- for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) {
- ui8a[i] = data.charCodeAt(i);
- }
- return applyBlob(new Blob([ui8a], {
- type: 'image/png'
- }));
+ });
};
fileURL = URL.createObjectURL(this.file);
return img.src = fileURL;
@@ -6500,7 +6538,7 @@
},
menu: {
init: function() {
- var conf, createSubEntry, el, key, subEntries, _ref;
+ var conf, createSubEntry, el, name, subEntries, _ref;
if (g.VIEW === 'catalog' || !Conf['Image Expansion']) {
return;
@@ -6512,9 +6550,9 @@
createSubEntry = ImageExpand.menu.createSubEntry;
subEntries = [];
_ref = Config.imageExpansion;
- for (key in _ref) {
- conf = _ref[key];
- subEntries.push(createSubEntry(key, conf));
+ for (name in _ref) {
+ conf = _ref[name];
+ subEntries.push(createSubEntry(name, conf[1]));
}
return $.event('AddMenuEntry', {
type: 'header',
@@ -6523,22 +6561,20 @@
subEntries: subEntries
});
},
- createSubEntry: function(type, config) {
+ createSubEntry: function(name, desc) {
var input, label;
label = $.el('label', {
- innerHTML: " " + type
+ innerHTML: " " + name,
+ title: desc
});
input = label.firstElementChild;
- if (type === 'Fit width' || type === 'Fit height') {
+ if (name === 'Fit width' || name === 'Fit height') {
$.on(input, 'change', ImageExpand.cb.setFitness);
}
- if (config) {
- label.title = config[1];
- input.checked = Conf[type];
- $.event('change', null, input);
- $.on(input, 'change', $.cb.checked);
- }
+ input.checked = Conf[name];
+ $.event('change', null, input);
+ $.on(input, 'change', $.cb.checked);
return {
el: label
};
@@ -7215,7 +7251,6 @@
this.postCountEl = $('#post-count', sc);
this.fileCountEl = $('#file-count', sc);
this.pageCountEl = $('#page-count', sc);
- this.lastModified = '0';
return Thread.prototype.callbacks.push({
name: 'Thread Stats',
cb: this.node
@@ -7270,19 +7305,13 @@
return $.ajax("//api.4chan.org/" + ThreadStats.thread.board + "/threads.json", {
onload: ThreadStats.onThreadsLoad
}, {
- headers: {
- 'If-Modified-Since': ThreadStats.lastModified
- }
+ whenModified: true
});
},
onThreadsLoad: function() {
var page, pages, thread, _i, _j, _len, _len1, _ref;
- if (!Conf["Page Count in Stats"]) {
- return;
- }
- ThreadStats.lastModified = this.getResponseHeader('Last-Modified');
- if (this.status !== 200) {
+ if (!(Conf["Page Count in Stats"] && this.status === 200)) {
return;
}
pages = JSON.parse(this.response);
@@ -7377,7 +7406,6 @@
ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0];
ThreadUpdater.outdateCount = 0;
- ThreadUpdater.lastModified = '0';
ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval']
}));
@@ -7472,10 +7500,7 @@
case 200:
g.DEAD = false;
ThreadUpdater.parse(JSON.parse(req.response).posts);
- ThreadUpdater.lastModified = req.getResponseHeader('Last-Modified');
- if (Conf['Auto Update']) {
- ThreadUpdater.set('timer', ThreadUpdater.getInterval());
- }
+ ThreadUpdater.set('timer', ThreadUpdater.getInterval());
break;
case 404:
g.DEAD = true;
@@ -7489,16 +7514,8 @@
});
break;
default:
- if (Conf['Auto Update']) {
- ThreadUpdater.outdateCount++;
- ThreadUpdater.set('timer', ThreadUpdater.getInterval());
- }
- /*
- Status Code 304: Not modified
- By sending the `If-Modified-Since` header we get a proper status code, and no response.
- This saves bandwidth for both the user and the servers and avoid unnecessary computation.
- */
-
+ ThreadUpdater.outdateCount++;
+ ThreadUpdater.set('timer', ThreadUpdater.getInterval());
_ref = req.status === 304 ? [null, null] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
ThreadUpdater.set('status', text, klass);
}
@@ -7571,9 +7588,7 @@
return ThreadUpdater.req = $.ajax(url, {
onloadend: ThreadUpdater.cb.load
}, {
- headers: {
- 'If-Modified-Since': ThreadUpdater.lastModified
- }
+ whenModified: true
});
},
updateThreadStatus: function(title, OP) {
@@ -7711,32 +7726,46 @@
ThreadWatcher = {
init: function() {
- var sc;
+ var now;
if (!Conf['Thread Watcher']) {
return;
}
- this.shortcut = sc = $.el('a', {
- textContent: 'Watcher',
- id: 'watcher-link',
- href: 'javascript:;',
- className: 'disabled'
- });
- this.dialog = UI.dialog('watcher', 'top: 50px; left: 0px;', '
');
+ this.db = new DataBoard('watchedThreads', this.refresh, true);
+ this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "Thread Watcher
");
+ this.status = $('#watcher-status', this.dialog);
+ this.list = this.dialog.lastElementChild;
$.on(d, 'QRPostSuccessful', this.cb.post);
- $.sync('WatchedThreads', this.refresh);
- $.on(sc, 'click', this.toggleWatcher);
- $.on($('.move>.close', ThreadWatcher.dialog), 'click', this.toggleWatcher);
- if (Conf['Toggleable Thread Watcher']) {
- Header.addShortcut(sc);
- $.addClass(doc, 'fixed-watcher');
+ if (g.VIEW === 'thread') {
+ $.on(d, 'ThreadUpdate', this.cb.threadUpdate);
}
- $.ready(function() {
- ThreadWatcher.refresh();
- $.add(d.body, ThreadWatcher.dialog);
- if (Conf['Toggleable Thread Watcher']) {
- return ThreadWatcher.dialog.hidden = true;
+ $.on(d, '4chanXInitFinished', this.ready);
+ now = Date.now();
+ if ((this.db.data.lastChecked || 0) < now - 2 * $.HOUR) {
+ this.db.data.lastChecked = now;
+ ThreadWatcher.fetchAllStatus();
+ this.db.save();
+ }
+ $.get('WatchedThreads', null, function(_arg) {
+ var WatchedThreads, boardID, data, threadID, threads, _ref;
+
+ WatchedThreads = _arg.WatchedThreads;
+ if (!WatchedThreads) {
+ return;
}
+ _ref = ThreadWatcher.convert(WatchedThreads);
+ for (boardID in _ref) {
+ threads = _ref[boardID];
+ for (threadID in threads) {
+ data = threads[threadID];
+ ThreadWatcher.db.set({
+ boardID: boardID,
+ threadID: threadID,
+ val: data
+ });
+ }
+ }
+ return $["delete"]('WatchedThreads');
});
return Thread.prototype.callbacks.push({
name: 'Thread Watcher',
@@ -7744,79 +7773,88 @@
});
},
node: function() {
- var favicon,
- _this = this;
+ var toggler;
- favicon = $.el('a', {
- className: 'watch-thread-link',
- href: 'javascript:;'
+ toggler = $.el('img', {
+ className: 'watcher-toggler'
});
- $.on(favicon, 'click', ThreadWatcher.cb.toggle);
- $.before($('input', this.OP.nodes.post), favicon);
- if (g.VIEW !== 'thread') {
+ $.on(toggler, 'click', ThreadWatcher.cb.toggle);
+ return $.before($('input', this.OP.nodes.post), toggler);
+ },
+ ready: function() {
+ $.off(d, '4chanXInitFinished', ThreadWatcher.ready);
+ if (!Main.isThisPageLegit()) {
return;
}
- return $.get('AutoWatch', 0, function(item) {
- if (item['AutoWatch'] !== _this.ID) {
+ ThreadWatcher.refresh();
+ $.add(d.body, ThreadWatcher.dialog);
+ if (!Conf['Auto Watch']) {
+ return;
+ }
+ return $.get('AutoWatch', 0, function(_arg) {
+ var AutoWatch, thread;
+
+ AutoWatch = _arg.AutoWatch;
+ if (!(thread = g.BOARD.threads[AutoWatch])) {
return;
}
- ThreadWatcher.watch(_this);
+ ThreadWatcher.add(thread);
return $["delete"]('AutoWatch');
});
},
- refresh: function(watched) {
- var ID, board, div, favicon, id, link, nodes, props, thread, x, _ref, _ref1;
-
- if (!watched) {
- $.get('WatchedThreads', {}, function(_arg) {
- var WatchedThreads;
-
- WatchedThreads = _arg.WatchedThreads;
- return ThreadWatcher.refresh(WatchedThreads);
- });
- return;
- }
- nodes = [$('.move', ThreadWatcher.dialog)];
- for (board in watched) {
- _ref = watched[board];
- for (id in _ref) {
- props = _ref[id];
- x = $.el('a', {
- textContent: '×',
- className: 'close',
- href: 'javascript:;'
- });
- $.on(x, 'click', ThreadWatcher.cb.x);
- link = $.el('a', props);
- link.title = link.textContent;
- div = $.el('div');
- $.add(div, [x, $.tn(' '), link]);
- nodes.push(div);
- }
- }
- $.rmAll(ThreadWatcher.dialog);
- $.add(ThreadWatcher.dialog, nodes);
- watched = watched[g.BOARD] || {};
- _ref1 = g.BOARD.threads;
- for (ID in _ref1) {
- thread = _ref1[ID];
- favicon = $('.watch-thread-link', thread.OP.nodes.post);
- $[ID in watched ? 'addClass' : 'rmClass'](favicon, 'watched');
- }
- },
toggleWatcher: function() {
$.toggleClass(ThreadWatcher.shortcut, 'disabled');
return ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden;
},
cb: {
+ openAll: function() {
+ var a, _i, _len, _ref;
+
+ if ($.hasClass(this, 'disabled')) {
+ return;
+ }
+ _ref = $$('a[title]', ThreadWatcher.list);
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ a = _ref[_i];
+ $.open(a.href);
+ }
+ 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')) {
+ return;
+ }
+ _ref = ThreadWatcher.getAll();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ _ref1 = _ref[_i], boardID = _ref1.boardID, threadID = _ref1.threadID, data = _ref1.data;
+ if (!data.isDead) {
+ continue;
+ }
+ delete ThreadWatcher.db.data.boards[boardID][threadID];
+ ThreadWatcher.db.deleteIfEmpty({
+ boardID: boardID
+ });
+ }
+ ThreadWatcher.db.save();
+ ThreadWatcher.refresh();
+ return $.event('CloseMenu');
+ },
toggle: function() {
return ThreadWatcher.toggle(Get.postFromNode(this).thread);
},
- x: function() {
- var thread;
+ rm: function() {
+ var boardID, threadID, _ref;
- thread = this.nextElementSibling.pathname.split('/');
- return ThreadWatcher.unwatch(thread[1], thread[3]);
+ _ref = this.parentNode.dataset.fullID.split('.'), boardID = _ref[0], threadID = _ref[1];
+ return ThreadWatcher.rm(boardID, +threadID);
},
post: function(e) {
var board, postID, threadID, _ref;
@@ -7827,43 +7865,346 @@
return $.set('AutoWatch', threadID);
}
} else if (Conf['Auto Watch Reply']) {
- return ThreadWatcher.watch(board.threads[threadID]);
+ return ThreadWatcher.add(board.threads[threadID]);
}
+ },
+ threadUpdate: function(e) {
+ var thread;
+
+ thread = e.detail.thread;
+ if (!(e.detail[404] && ThreadWatcher.db.get({
+ boardID: thread.board.ID,
+ threadID: thread.ID
+ }))) {
+ return;
+ }
+ return ThreadWatcher.add(thread);
+ }
+ },
+ fetchCount: {
+ fetched: 0,
+ fetching: 0
+ },
+ fetchAllStatus: function() {
+ var thread, _i, _len, _ref;
+
+ ThreadWatcher.status.textContent = '...';
+ _ref = ThreadWatcher.getAll();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ thread = _ref[_i];
+ ThreadWatcher.fetchStatus(thread);
+ }
+ },
+ fetchStatus: function(_arg) {
+ var boardID, data, fetchCount, threadID;
+
+ boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data;
+ if (data.isDead) {
+ return;
+ }
+ fetchCount = ThreadWatcher.fetchCount;
+ fetchCount.fetching++;
+ return $.ajax("//api.4chan.org/" + boardID + "/res/" + threadID + ".json", {
+ onloadend: function() {
+ var status;
+
+ fetchCount.fetched++;
+ if (fetchCount.fetched === fetchCount.fetching) {
+ fetchCount.fetched = 0;
+ fetchCount.fetching = 0;
+ status = '';
+ } else {
+ status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%";
+ }
+ ThreadWatcher.status.textContent = status;
+ if (this.status !== 404) {
+ return;
+ }
+ if (Conf['Auto Prune']) {
+ ThreadWatcher.rm(boardID, threadID);
+ } else {
+ data.isDead = true;
+ ThreadWatcher.db.set({
+ boardID: boardID,
+ threadID: threadID,
+ val: data
+ });
+ }
+ return ThreadWatcher.refresh();
+ }
+ }, {
+ type: 'head'
+ });
+ },
+ getAll: function() {
+ var all, boardID, data, threadID, threads, _ref;
+
+ all = [];
+ _ref = ThreadWatcher.db.data.boards;
+ for (boardID in _ref) {
+ threads = _ref[boardID];
+ if (Conf['Current Board'] && boardID !== g.BOARD.ID) {
+ continue;
+ }
+ for (threadID in threads) {
+ data = threads[threadID];
+ all.push({
+ boardID: boardID,
+ threadID: threadID,
+ data: data
+ });
+ }
+ }
+ return all;
+ },
+ makeLine: function(boardID, threadID, data) {
+ var div, fullID, href, link, x;
+
+ x = $.el('a', {
+ textContent: '×',
+ href: 'javascript:;'
+ });
+ $.on(x, 'click', ThreadWatcher.cb.rm);
+ if (data.isDead) {
+ href = Redirect.to('thread', {
+ boardID: boardID,
+ threadID: threadID
+ });
+ }
+ link = $.el('a', {
+ href: href || ("/" + boardID + "/res/" + threadID),
+ textContent: data.excerpt,
+ title: data.excerpt
+ });
+ div = $.el('div');
+ fullID = "" + boardID + "." + threadID;
+ div.dataset.fullID = fullID;
+ if (g.VIEW === 'thread' && fullID === ("" + g.BOARD + "." + g.THREADID)) {
+ $.addClass(div, 'current');
+ }
+ if (data.isDead) {
+ $.addClass(div, 'dead-thread');
+ }
+ $.add(div, [x, $.tn(' '), link]);
+ return div;
+ },
+ refresh: function() {
+ var boardID, data, list, nodes, refresher, thread, threadID, toggler, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
+
+ nodes = [];
+ _ref = ThreadWatcher.getAll();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ _ref1 = _ref[_i], boardID = _ref1.boardID, threadID = _ref1.threadID, data = _ref1.data;
+ nodes.push(ThreadWatcher.makeLine(boardID, threadID, data));
+ }
+ list = ThreadWatcher.list;
+ $.rmAll(list);
+ $.add(list, nodes);
+ _ref2 = g.BOARD.threads;
+ for (threadID in _ref2) {
+ thread = _ref2[threadID];
+ toggler = $('.watcher-toggler', thread.OP.nodes.post);
+ toggler.src = ThreadWatcher.db.get({
+ boardID: thread.board.ID,
+ threadID: threadID
+ }) ? Favicon["default"] : Favicon.empty;
+ }
+ _ref3 = ThreadWatcher.menu.refreshers;
+ for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
+ refresher = _ref3[_j];
+ refresher();
}
},
toggle: function(thread) {
- if (!$.hasClass($('.watch-thread-link', thread.OP.nodes.post), 'watched')) {
- return ThreadWatcher.watch(thread);
+ var boardID, threadID;
+
+ boardID = thread.board.ID;
+ threadID = thread.ID;
+ if (ThreadWatcher.db.get({
+ boardID: boardID,
+ threadID: threadID
+ })) {
+ return ThreadWatcher.rm(boardID, threadID);
} else {
- return ThreadWatcher.unwatch(thread.board, thread.ID);
+ return ThreadWatcher.add(thread);
}
},
- unwatch: function(board, threadID) {
- return $.get('WatchedThreads', {}, function(item) {
- var watched;
+ add: function(thread) {
+ var boardID, data, threadID;
- watched = item['WatchedThreads'];
- delete watched[board][threadID];
- if (!Object.keys(watched[board]).length) {
- delete watched[board];
+ data = {};
+ boardID = thread.board.ID;
+ threadID = thread.ID;
+ if (thread.isDead) {
+ if (Conf['Auto Prune'] && ThreadWatcher.db.get({
+ boardID: boardID,
+ threadID: threadID
+ })) {
+ ThreadWatcher.rm(boardID, threadID);
+ return;
}
- ThreadWatcher.refresh(watched);
- return $.set('WatchedThreads', watched);
+ data.isDead = true;
+ }
+ data.excerpt = Get.threadExcerpt(thread);
+ ThreadWatcher.db.set({
+ boardID: boardID,
+ threadID: threadID,
+ val: data
});
+ return ThreadWatcher.refresh();
},
- watch: function(thread) {
- return $.get('WatchedThreads', {}, function(item) {
- var watched, _name;
-
- watched = item['WatchedThreads'];
- watched[_name = thread.board] || (watched[_name] = {});
- watched[thread.board][thread] = {
- href: "/" + thread.board + "/res/" + thread,
- textContent: Get.threadExcerpt(thread)
- };
- ThreadWatcher.refresh(watched);
- return $.set('WatchedThreads', watched);
+ rm: function(boardID, threadID) {
+ ThreadWatcher.db["delete"]({
+ boardID: boardID,
+ threadID: threadID
});
+ return ThreadWatcher.refresh();
+ },
+ convert: function(oldFormat) {
+ var boardID, data, newFormat, threadID, threads;
+
+ newFormat = {};
+ for (boardID in oldFormat) {
+ threads = oldFormat[boardID];
+ for (threadID in threads) {
+ data = threads[threadID];
+ (newFormat[boardID] || (newFormat[boardID] = {}))[threadID] = {
+ excerpt: data.textContent
+ };
+ }
+ }
+ return newFormat;
+ },
+ menu: {
+ refreshers: [],
+ init: function() {
+ var menu;
+
+ if (!Conf['Thread Watcher']) {
+ return;
+ }
+ 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();
+ },
+ addHeaderMenuEntry: function() {
+ var entryEl;
+
+ if (g.VIEW !== 'thread') {
+ return;
+ }
+ entryEl = $.el('a', {
+ href: 'javascript:;'
+ });
+ $.event('AddMenuEntry', {
+ type: 'header',
+ el: entryEl,
+ order: 60
+ });
+ $.on(entryEl, 'click', function() {
+ return ThreadWatcher.toggle(g.threads["" + g.BOARD + "." + g.THREADID]);
+ });
+ return this.refreshers.push(function() {
+ var addClass, rmClass, text, _ref;
+
+ _ref = $('.current', ThreadWatcher.list) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = _ref[0], rmClass = _ref[1], text = _ref[2];
+ $.addClass(entryEl, addClass);
+ $.rmClass(entryEl, rmClass);
+ return entryEl.textContent = text;
+ });
+ },
+ addMenuEntries: function() {
+ var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1, _results;
+
+ entries = [];
+ entries.push({
+ cb: ThreadWatcher.cb.openAll,
+ entry: {
+ type: 'thread watcher',
+ el: $.el('a', {
+ textContent: 'Open all threads'
+ })
+ },
+ refresh: function() {
+ return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled');
+ }
+ });
+ entries.push({
+ cb: ThreadWatcher.cb.checkThreads,
+ entry: {
+ type: 'thread watcher',
+ 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: {
+ type: 'thread watcher',
+ el: $.el('a', {
+ textContent: 'Prune 404\'d threads'
+ })
+ },
+ refresh: function() {
+ return ($('.dead-thread', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled');
+ }
+ });
+ subEntries = [];
+ _ref = Config.threadWatcher;
+ for (name in _ref) {
+ conf = _ref[name];
+ subEntries.push(this.createSubEntry(name, conf[1]));
+ }
+ entries.push({
+ entry: {
+ type: 'thread watcher',
+ el: $.el('span', {
+ textContent: 'Settings'
+ }),
+ subEntries: subEntries
+ }
+ });
+ _results = [];
+ for (_i = 0, _len = entries.length; _i < _len; _i++) {
+ _ref1 = entries[_i], entry = _ref1.entry, cb = _ref1.cb, refresh = _ref1.refresh;
+ if (entry.el.nodeName === 'A') {
+ entry.el.href = 'javascript:;';
+ }
+ if (cb) {
+ $.on(entry.el, 'click', cb);
+ }
+ if (refresh) {
+ this.refreshers.push(refresh.bind(entry));
+ }
+ _results.push($.event('AddMenuEntry', entry));
+ }
+ return _results;
+ },
+ createSubEntry: function(name, desc) {
+ var entry, input;
+
+ entry = {
+ type: 'thread watcher',
+ el: $.el('label', {
+ innerHTML: " " + name,
+ title: desc
+ })
+ };
+ input = entry.el.firstElementChild;
+ input.checked = Conf[name];
+ $.on(input, 'change', $.cb.checked);
+ if (name === 'Current Board') {
+ $.on(input, 'change', ThreadWatcher.refresh);
+ }
+ return entry;
+ }
}
};
@@ -8628,11 +8969,8 @@
},
callbacks: [],
cb: function(e) {
- var post;
-
e.preventDefault();
- post = Get.postFromNode(this);
- return ExpandComment.expand(post);
+ return ExpandComment.expand(Get.postFromNode(this));
},
expand: function(post) {
var a;
@@ -8695,6 +9033,12 @@
}
quote.href = "/" + post.board + "/res/" + href;
}
+ Build.capcodeReplies({
+ boardID: post.board.ID,
+ threadID: post.thread.ID,
+ bq: clone,
+ capcodeReplies: postObj.capcode_replies
+ });
post.nodes.shortComment = comment;
$.replace(comment, clone);
post.nodes.comment = post.nodes.longComment = clone;
@@ -9737,7 +10081,8 @@
$.on(d, '4chanXInitFinished', Settings.open);
}
return $.set({
- lastchecked: Date.now(),
+ archives: Conf['archives'],
+ lastarchivecheck: now,
previousversion: g.VERSION
});
});
@@ -9932,7 +10277,6 @@
version: g.VERSION,
date: now
};
- Conf['WatchedThreads'] = {};
for (_i = 0, _len = DataBoards.length; _i < _len; _i++) {
db = DataBoards[_i];
Conf[db] = {
@@ -10067,14 +10411,13 @@
});
}
}
- data.Conf.WatchedThreads = data.WatchedThreads;
- } else if (version[0] === '3') {
- data = Settings.convertSettings(data, {
- 'Reply Hiding': 'Reply Hiding Buttons',
- 'Thread Hiding': 'Thread Hiding Buttons',
- 'Bottom header': 'Bottom Header',
- 'Unread Tab Icon': 'Unread Favicon'
- });
+ data.Conf['WatchedThreads'] = data.WatchedThreads;
+ }
+ if (data.Conf['WatchedThreads']) {
+ data.Conf['watchedThreads'] = {
+ boards: ThreadWatcher.convert(data.Conf['WatchedThreads'])
+ };
+ delete data.Conf['WatchedThreads'];
}
return $.set(data.Conf);
},
@@ -10521,6 +10864,7 @@
'Thread Stats': ThreadStats,
'Thread Updater': ThreadUpdater,
'Thread Watcher': ThreadWatcher,
+ 'Thread Watcher (Menu)': ThreadWatcher.menu,
'Index Navigation': Nav,
'Keybinds': Keybinds,
'Show Dice Roll': Dice
@@ -10618,8 +10962,7 @@
}
Main.callbackNodes(Thread, threads);
Main.callbackNodesDB(Post, posts, function() {
- $.event('4chanXInitFinished');
- return Main.checkUpdate();
+ return $.event('4chanXInitFinished');
});
if (styleSelector = $.id('styleSelector')) {
passLink = $.el('a', {
@@ -10639,8 +10982,7 @@
err = _error;
new Notification('warning', 'Cookies need to be enabled on 4chan for 4chan X to properly function.', 30);
}
- $.event('4chanXInitFinished');
- return Main.checkUpdate();
+ return $.event('4chanXInitFinished');
},
callbackNodes: function(klass, nodes) {
var callback, err, errors, i, len, node, _i, _len, _ref;
@@ -10747,40 +11089,6 @@
obj.callback.isAddon = true;
return Klass.prototype.callbacks.push(obj.callback);
},
- message: function(e) {
- var el, version;
-
- version = e.data.version;
- if (version && version !== g.VERSION) {
- el = $.el('span', {
- innerHTML: "Update: 4chan X v" + version + " is out, get it here."
- });
- return new Notification('info', el, 120);
- }
- },
- checkUpdate: function() {
- var now;
-
- if (!(Conf['Check for Updates'] && Main.isThisPageLegit())) {
- return;
- }
- now = Date.now();
- return $.get('lastchecked', 0, function(_arg) {
- var lastchecked;
-
- lastchecked = _arg.lastchecked;
- if (lastchecked > now - $.DAY) {
- return;
- }
- return $.ready(function() {
- $.on(window, 'message', Main.message);
- $.set('lastchecked', now);
- return $.add(d.head, $.el('script', {
- src: 'https://github.com/seaweedchan/4chan-x/raw/master/latest.js'
- }));
- });
- });
- },
handleErrors: function(errors) {
var div, error, logs, _i, _len;
diff --git a/builds/crx/script.js b/builds/crx/script.js
index 478542ca3..48050ef40 100644
--- a/builds/crx/script.js
+++ b/builds/crx/script.js
@@ -170,9 +170,7 @@
'Page Count in Stats': [false, 'Display the page count in the thread stats as well.'],
'Updater and Stats in Header': [true, 'Places the thread updater and thread stats in the header instead of floating them.'],
'Thread Watcher': [true, 'Bookmark threads.'],
- 'Toggleable Thread Watcher': [true, 'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.'],
- 'Auto Watch': [true, 'Automatically watch threads you start.'],
- 'Auto Watch Reply': [false, 'Automatically watch threads you reply to.']
+ 'Toggleable Thread Watcher': [true, 'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.']
},
'Posting': {
'Quick Reply': [true, 'All-in-one form to reply, create threads, automate dumping and more.'],
@@ -212,6 +210,12 @@
'Expand from here': [false, 'Expand all images only from current position to thread end.'],
'Advance on contract': [false, 'Advance to next post when contracting an expanded image.']
},
+ threadWatcher: {
+ 'Current Board': [false, 'Only show watched threads from the current board.'],
+ 'Auto Watch': [true, 'Automatically watch threads you start.'],
+ 'Auto Watch Reply': [false, 'Automatically watch threads you reply to.'],
+ 'Auto Prune': [false, 'Automatically prune 404\'d threads.']
+ },
filter: {
name: "# Filter any namefags:\n#/^(?!Anonymous$)/",
uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/",
@@ -404,26 +408,32 @@
}
};
- $.ajax = function(url, options, extra) {
- var form, headers, key, r, sync, type, upCallbacks, val;
+ $.ajax = (function() {
+ var lastModified;
- if (extra == null) {
- extra = {};
- }
- type = extra.type, headers = extra.headers, upCallbacks = extra.upCallbacks, form = extra.form, sync = extra.sync;
- r = new XMLHttpRequest();
- r.overrideMimeType('text/html');
- type || (type = form && 'post' || 'get');
- r.open(type, url, !sync);
- for (key in headers) {
- val = headers[key];
- r.setRequestHeader(key, val);
- }
- $.extend(r, options);
- $.extend(r.upload, upCallbacks);
- r.send(form);
- return r;
- };
+ lastModified = {};
+ return function(url, options, extra) {
+ var form, r, sync, type, upCallbacks, whenModified;
+
+ if (extra == null) {
+ extra = {};
+ }
+ type = extra.type, whenModified = extra.whenModified, upCallbacks = extra.upCallbacks, form = extra.form, sync = extra.sync;
+ r = new XMLHttpRequest();
+ type || (type = form && 'post' || 'get');
+ r.open(type, url, !sync);
+ if (whenModified) {
+ r.setRequestHeader('If-Modified-Since', lastModified[url] || '0');
+ $.on(r, 'load', function() {
+ return lastModified[url] = r.getResponseHeader('Last-Modified');
+ });
+ }
+ $.extend(r, options);
+ $.extend(r.upload, upCallbacks);
+ r.send(form);
+ return r;
+ };
+ })();
$.cache = (function() {
var reqs;
@@ -1210,17 +1220,19 @@
})(Post);
- DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts'];
+ DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads'];
DataBoard = (function() {
- function DataBoard(key, sync) {
+ function DataBoard(key, sync, dontClean) {
var init,
_this = this;
this.key = key;
this.data = Conf[key];
$.sync(key, this.onSync.bind(this));
- this.clean();
+ if (!dontClean) {
+ this.clean();
+ }
if (!sync) {
return;
}
@@ -1231,6 +1243,10 @@
$.on(d, '4chanXInitFinished', init);
}
+ DataBoard.prototype.save = function() {
+ return $.set(this.key, this.data);
+ };
+
DataBoard.prototype["delete"] = function(_arg) {
var boardID, postID, threadID;
@@ -1249,7 +1265,7 @@
} else {
delete this.data.boards[boardID];
}
- return $.set(this.key, this.data);
+ return this.save();
};
DataBoard.prototype.deleteIfEmpty = function(_arg) {
@@ -1279,7 +1295,7 @@
} else {
this.data.boards[boardID] = val;
}
- return $.set(this.key, this.data);
+ return this.save();
};
DataBoard.prototype.get = function(_arg) {
@@ -1323,7 +1339,7 @@
this.ajaxClean(boardID);
}
}
- return $.set(this.key, this.data);
+ return this.save();
};
DataBoard.prototype.ajaxClean = function(boardID) {
@@ -1353,7 +1369,7 @@
boardID: boardID
});
}
- return $.set(_this.key, _this.data);
+ return _this.save();
});
};
@@ -1414,8 +1430,26 @@
Polyfill = {
init: function() {
+ Polyfill.toBlob();
return Polyfill.visibility();
},
+ toBlob: function() {
+ var _base;
+
+ return (_base = HTMLCanvasElement.prototype).toBlob || (_base.toBlob = function(cb) {
+ var data, i, l, ui8a, _i;
+
+ data = atob(this.toDataURL().slice(22));
+ l = data.length;
+ ui8a = new Uint8Array(l);
+ for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) {
+ ui8a[i] = data.charCodeAt(i);
+ }
+ return cb(new Blob([ui8a], {
+ type: 'image/png'
+ }));
+ });
+ },
visibility: function() {
if (!('webkitHidden' in document)) {
return;
@@ -1834,7 +1868,7 @@
date: data.now,
dateUTC: data.time,
comment: data.com,
- capReps: data.capcode_replies,
+ capcodeReplies: data.capcode_replies,
isSticky: !!data.sticky,
isClosed: !!data.closed
};
@@ -1862,9 +1896,9 @@
@license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
*/
- var a, array, boardID, capReps, capcode, capcodeClass, capcodeReplies, capcodeStart, capcodeType, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, generateCapcodeReplies, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref;
+ var a, boardID, capcode, capcodeClass, capcodeReplies, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref;
- postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, capReps = o.capReps, file = o.file;
+ postID = o.postID, threadID = o.threadID, boardID = o.boardID, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, capcodeReplies = o.capcodeReplies, file = o.file;
isOP = postID === threadID;
staticPath = '//static.4chan.org/image/';
if (email) {
@@ -1938,28 +1972,6 @@
tripcode = tripcode ? " " + tripcode + "" : '';
sticky = isSticky ? "
" : '';
closed = isClosed ? "
" : '';
- capcodeReplies = '';
- if (capReps) {
- generateCapcodeReplies = function(capcodeType, array) {
- return "" + ((function() {
- switch (capcodeType) {
- case 'admin':
- return 'Administrator';
- case 'mod':
- return 'Moderator';
- case 'developer':
- return 'Developer';
- }
- })()) + " Repl" + (array.length > 1 ? 'ies' : 'y') + ": " + (array.map(function(ID) {
- return ">>" + ID + "";
- }).join(' ')) + "
";
- };
- for (capcodeType in capReps) {
- array = capReps[capcodeType];
- capcodeReplies += generateCapcodeReplies(capcodeType, array);
- }
- capcodeReplies = "
" + capcodeReplies + "";
- }
container = $.el('div', {
id: "pc" + postID,
className: "postContainer " + (isOP ? 'op' : 'reply') + "Container",
@@ -1974,7 +1986,47 @@
}
quote.href = "/" + boardID + "/res/" + href;
}
+ Build.capcodeReplies({
+ boardID: boardID,
+ threadID: threadID,
+ root: container,
+ capcodeReplies: capcodeReplies
+ });
return container;
+ },
+ capcodeReplies: function(_arg) {
+ var array, boardID, bq, capcodeReplies, capcodeType, generateCapcodeReplies, html, root, threadID;
+
+ boardID = _arg.boardID, threadID = _arg.threadID, bq = _arg.bq, root = _arg.root, capcodeReplies = _arg.capcodeReplies;
+ if (!capcodeReplies) {
+ return;
+ }
+ generateCapcodeReplies = function(capcodeType, array) {
+ return "" + ((function() {
+ switch (capcodeType) {
+ case 'admin':
+ return 'Administrator';
+ case 'mod':
+ return 'Moderator';
+ case 'developer':
+ return 'Developer';
+ }
+ })()) + " Repl" + (array.length > 1 ? 'ies' : 'y') + ": " + (array.map(function(ID) {
+ return ">>" + ID + "";
+ }).join(' ')) + "
";
+ };
+ html = [];
+ for (capcodeType in capcodeReplies) {
+ array = capcodeReplies[capcodeType];
+ html.push(generateCapcodeReplies(capcodeType, array));
+ }
+ bq || (bq = $('blockquote', root));
+ return $.add(bq, [
+ $.el('br'), $.el('br'), $.el('span', {
+ className: 'capcodeReplies',
+ innerHTML: html.join('')
+ })
+ ]);
}
};
@@ -5607,7 +5659,7 @@
img = $.el('img');
img.onload = function() {
- var applyBlob, cv, data, height, i, l, s, ui8a, width, _i;
+ var cv, height, s, width;
s = 90 * 2;
if (_this.file.type === 'image/gif') {
@@ -5631,23 +5683,10 @@
cv.width = img.width = width;
cv.getContext('2d').drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(fileURL);
- applyBlob = function(blob) {
+ return cv.toBlob(function(blob) {
_this.URL = URL.createObjectURL(blob);
return _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
- };
- if (cv.toBlob) {
- cv.toBlob(applyBlob);
- return;
- }
- data = atob(cv.toDataURL().split(',')[1]);
- l = data.length;
- ui8a = new Uint8Array(l);
- for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) {
- ui8a[i] = data.charCodeAt(i);
- }
- return applyBlob(new Blob([ui8a], {
- type: 'image/png'
- }));
+ });
};
fileURL = URL.createObjectURL(this.file);
return img.src = fileURL;
@@ -6481,7 +6520,7 @@
},
menu: {
init: function() {
- var conf, createSubEntry, el, key, subEntries, _ref;
+ var conf, createSubEntry, el, name, subEntries, _ref;
if (g.VIEW === 'catalog' || !Conf['Image Expansion']) {
return;
@@ -6493,9 +6532,9 @@
createSubEntry = ImageExpand.menu.createSubEntry;
subEntries = [];
_ref = Config.imageExpansion;
- for (key in _ref) {
- conf = _ref[key];
- subEntries.push(createSubEntry(key, conf));
+ for (name in _ref) {
+ conf = _ref[name];
+ subEntries.push(createSubEntry(name, conf[1]));
}
return $.event('AddMenuEntry', {
type: 'header',
@@ -6504,22 +6543,20 @@
subEntries: subEntries
});
},
- createSubEntry: function(type, config) {
+ createSubEntry: function(name, desc) {
var input, label;
label = $.el('label', {
- innerHTML: " " + type
+ innerHTML: " " + name,
+ title: desc
});
input = label.firstElementChild;
- if (type === 'Fit width' || type === 'Fit height') {
+ if (name === 'Fit width' || name === 'Fit height') {
$.on(input, 'change', ImageExpand.cb.setFitness);
}
- if (config) {
- label.title = config[1];
- input.checked = Conf[type];
- $.event('change', null, input);
- $.on(input, 'change', $.cb.checked);
- }
+ input.checked = Conf[name];
+ $.event('change', null, input);
+ $.on(input, 'change', $.cb.checked);
return {
el: label
};
@@ -7196,7 +7233,6 @@
this.postCountEl = $('#post-count', sc);
this.fileCountEl = $('#file-count', sc);
this.pageCountEl = $('#page-count', sc);
- this.lastModified = '0';
return Thread.prototype.callbacks.push({
name: 'Thread Stats',
cb: this.node
@@ -7251,19 +7287,13 @@
return $.ajax("//api.4chan.org/" + ThreadStats.thread.board + "/threads.json", {
onload: ThreadStats.onThreadsLoad
}, {
- headers: {
- 'If-Modified-Since': ThreadStats.lastModified
- }
+ whenModified: true
});
},
onThreadsLoad: function() {
var page, pages, thread, _i, _j, _len, _len1, _ref;
- if (!Conf["Page Count in Stats"]) {
- return;
- }
- ThreadStats.lastModified = this.getResponseHeader('Last-Modified');
- if (this.status !== 200) {
+ if (!(Conf["Page Count in Stats"] && this.status === 200)) {
return;
}
pages = JSON.parse(this.response);
@@ -7358,7 +7388,6 @@
ThreadUpdater.root = this.OP.nodes.root.parentNode;
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0];
ThreadUpdater.outdateCount = 0;
- ThreadUpdater.lastModified = '0';
ThreadUpdater.cb.interval.call($.el('input', {
value: Conf['Interval']
}));
@@ -7453,10 +7482,7 @@
case 200:
g.DEAD = false;
ThreadUpdater.parse(JSON.parse(req.response).posts);
- ThreadUpdater.lastModified = req.getResponseHeader('Last-Modified');
- if (Conf['Auto Update']) {
- ThreadUpdater.set('timer', ThreadUpdater.getInterval());
- }
+ ThreadUpdater.set('timer', ThreadUpdater.getInterval());
break;
case 404:
g.DEAD = true;
@@ -7470,16 +7496,8 @@
});
break;
default:
- if (Conf['Auto Update']) {
- ThreadUpdater.outdateCount++;
- ThreadUpdater.set('timer', ThreadUpdater.getInterval());
- }
- /*
- Status Code 304: Not modified
- By sending the `If-Modified-Since` header we get a proper status code, and no response.
- This saves bandwidth for both the user and the servers and avoid unnecessary computation.
- */
-
+ ThreadUpdater.outdateCount++;
+ ThreadUpdater.set('timer', ThreadUpdater.getInterval());
_ref = req.status === 304 ? [null, null] : ["" + req.statusText + " (" + req.status + ")", 'warning'], text = _ref[0], klass = _ref[1];
ThreadUpdater.set('status', text, klass);
}
@@ -7552,9 +7570,7 @@
return ThreadUpdater.req = $.ajax(url, {
onloadend: ThreadUpdater.cb.load
}, {
- headers: {
- 'If-Modified-Since': ThreadUpdater.lastModified
- }
+ whenModified: true
});
},
updateThreadStatus: function(title, OP) {
@@ -7692,32 +7708,46 @@
ThreadWatcher = {
init: function() {
- var sc;
+ var now;
if (!Conf['Thread Watcher']) {
return;
}
- this.shortcut = sc = $.el('a', {
- textContent: 'Watcher',
- id: 'watcher-link',
- href: 'javascript:;',
- className: 'disabled'
- });
- this.dialog = UI.dialog('watcher', 'top: 50px; left: 0px;', '');
+ this.db = new DataBoard('watchedThreads', this.refresh, true);
+ this.dialog = UI.dialog('thread-watcher', 'top: 50px; left: 0px;', "Thread Watcher
");
+ this.status = $('#watcher-status', this.dialog);
+ this.list = this.dialog.lastElementChild;
$.on(d, 'QRPostSuccessful', this.cb.post);
- $.sync('WatchedThreads', this.refresh);
- $.on(sc, 'click', this.toggleWatcher);
- $.on($('.move>.close', ThreadWatcher.dialog), 'click', this.toggleWatcher);
- if (Conf['Toggleable Thread Watcher']) {
- Header.addShortcut(sc);
- $.addClass(doc, 'fixed-watcher');
+ if (g.VIEW === 'thread') {
+ $.on(d, 'ThreadUpdate', this.cb.threadUpdate);
}
- $.ready(function() {
- ThreadWatcher.refresh();
- $.add(d.body, ThreadWatcher.dialog);
- if (Conf['Toggleable Thread Watcher']) {
- return ThreadWatcher.dialog.hidden = true;
+ $.on(d, '4chanXInitFinished', this.ready);
+ now = Date.now();
+ if ((this.db.data.lastChecked || 0) < now - 2 * $.HOUR) {
+ this.db.data.lastChecked = now;
+ ThreadWatcher.fetchAllStatus();
+ this.db.save();
+ }
+ $.get('WatchedThreads', null, function(_arg) {
+ var WatchedThreads, boardID, data, threadID, threads, _ref;
+
+ WatchedThreads = _arg.WatchedThreads;
+ if (!WatchedThreads) {
+ return;
}
+ _ref = ThreadWatcher.convert(WatchedThreads);
+ for (boardID in _ref) {
+ threads = _ref[boardID];
+ for (threadID in threads) {
+ data = threads[threadID];
+ ThreadWatcher.db.set({
+ boardID: boardID,
+ threadID: threadID,
+ val: data
+ });
+ }
+ }
+ return $["delete"]('WatchedThreads');
});
return Thread.prototype.callbacks.push({
name: 'Thread Watcher',
@@ -7725,79 +7755,88 @@
});
},
node: function() {
- var favicon,
- _this = this;
+ var toggler;
- favicon = $.el('a', {
- className: 'watch-thread-link',
- href: 'javascript:;'
+ toggler = $.el('img', {
+ className: 'watcher-toggler'
});
- $.on(favicon, 'click', ThreadWatcher.cb.toggle);
- $.before($('input', this.OP.nodes.post), favicon);
- if (g.VIEW !== 'thread') {
+ $.on(toggler, 'click', ThreadWatcher.cb.toggle);
+ return $.before($('input', this.OP.nodes.post), toggler);
+ },
+ ready: function() {
+ $.off(d, '4chanXInitFinished', ThreadWatcher.ready);
+ if (!Main.isThisPageLegit()) {
return;
}
- return $.get('AutoWatch', 0, function(item) {
- if (item['AutoWatch'] !== _this.ID) {
+ ThreadWatcher.refresh();
+ $.add(d.body, ThreadWatcher.dialog);
+ if (!Conf['Auto Watch']) {
+ return;
+ }
+ return $.get('AutoWatch', 0, function(_arg) {
+ var AutoWatch, thread;
+
+ AutoWatch = _arg.AutoWatch;
+ if (!(thread = g.BOARD.threads[AutoWatch])) {
return;
}
- ThreadWatcher.watch(_this);
+ ThreadWatcher.add(thread);
return $["delete"]('AutoWatch');
});
},
- refresh: function(watched) {
- var ID, board, div, favicon, id, link, nodes, props, thread, x, _ref, _ref1;
-
- if (!watched) {
- $.get('WatchedThreads', {}, function(_arg) {
- var WatchedThreads;
-
- WatchedThreads = _arg.WatchedThreads;
- return ThreadWatcher.refresh(WatchedThreads);
- });
- return;
- }
- nodes = [$('.move', ThreadWatcher.dialog)];
- for (board in watched) {
- _ref = watched[board];
- for (id in _ref) {
- props = _ref[id];
- x = $.el('a', {
- textContent: '×',
- className: 'close',
- href: 'javascript:;'
- });
- $.on(x, 'click', ThreadWatcher.cb.x);
- link = $.el('a', props);
- link.title = link.textContent;
- div = $.el('div');
- $.add(div, [x, $.tn(' '), link]);
- nodes.push(div);
- }
- }
- $.rmAll(ThreadWatcher.dialog);
- $.add(ThreadWatcher.dialog, nodes);
- watched = watched[g.BOARD] || {};
- _ref1 = g.BOARD.threads;
- for (ID in _ref1) {
- thread = _ref1[ID];
- favicon = $('.watch-thread-link', thread.OP.nodes.post);
- $[ID in watched ? 'addClass' : 'rmClass'](favicon, 'watched');
- }
- },
toggleWatcher: function() {
$.toggleClass(ThreadWatcher.shortcut, 'disabled');
return ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden;
},
cb: {
+ openAll: function() {
+ var a, _i, _len, _ref;
+
+ if ($.hasClass(this, 'disabled')) {
+ return;
+ }
+ _ref = $$('a[title]', ThreadWatcher.list);
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ a = _ref[_i];
+ $.open(a.href);
+ }
+ 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')) {
+ return;
+ }
+ _ref = ThreadWatcher.getAll();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ _ref1 = _ref[_i], boardID = _ref1.boardID, threadID = _ref1.threadID, data = _ref1.data;
+ if (!data.isDead) {
+ continue;
+ }
+ delete ThreadWatcher.db.data.boards[boardID][threadID];
+ ThreadWatcher.db.deleteIfEmpty({
+ boardID: boardID
+ });
+ }
+ ThreadWatcher.db.save();
+ ThreadWatcher.refresh();
+ return $.event('CloseMenu');
+ },
toggle: function() {
return ThreadWatcher.toggle(Get.postFromNode(this).thread);
},
- x: function() {
- var thread;
+ rm: function() {
+ var boardID, threadID, _ref;
- thread = this.nextElementSibling.pathname.split('/');
- return ThreadWatcher.unwatch(thread[1], thread[3]);
+ _ref = this.parentNode.dataset.fullID.split('.'), boardID = _ref[0], threadID = _ref[1];
+ return ThreadWatcher.rm(boardID, +threadID);
},
post: function(e) {
var board, postID, threadID, _ref;
@@ -7808,43 +7847,346 @@
return $.set('AutoWatch', threadID);
}
} else if (Conf['Auto Watch Reply']) {
- return ThreadWatcher.watch(board.threads[threadID]);
+ return ThreadWatcher.add(board.threads[threadID]);
}
+ },
+ threadUpdate: function(e) {
+ var thread;
+
+ thread = e.detail.thread;
+ if (!(e.detail[404] && ThreadWatcher.db.get({
+ boardID: thread.board.ID,
+ threadID: thread.ID
+ }))) {
+ return;
+ }
+ return ThreadWatcher.add(thread);
+ }
+ },
+ fetchCount: {
+ fetched: 0,
+ fetching: 0
+ },
+ fetchAllStatus: function() {
+ var thread, _i, _len, _ref;
+
+ ThreadWatcher.status.textContent = '...';
+ _ref = ThreadWatcher.getAll();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ thread = _ref[_i];
+ ThreadWatcher.fetchStatus(thread);
+ }
+ },
+ fetchStatus: function(_arg) {
+ var boardID, data, fetchCount, threadID;
+
+ boardID = _arg.boardID, threadID = _arg.threadID, data = _arg.data;
+ if (data.isDead) {
+ return;
+ }
+ fetchCount = ThreadWatcher.fetchCount;
+ fetchCount.fetching++;
+ return $.ajax("//api.4chan.org/" + boardID + "/res/" + threadID + ".json", {
+ onloadend: function() {
+ var status;
+
+ fetchCount.fetched++;
+ if (fetchCount.fetched === fetchCount.fetching) {
+ fetchCount.fetched = 0;
+ fetchCount.fetching = 0;
+ status = '';
+ } else {
+ status = "" + (Math.round(fetchCount.fetched / fetchCount.fetching * 100)) + "%";
+ }
+ ThreadWatcher.status.textContent = status;
+ if (this.status !== 404) {
+ return;
+ }
+ if (Conf['Auto Prune']) {
+ ThreadWatcher.rm(boardID, threadID);
+ } else {
+ data.isDead = true;
+ ThreadWatcher.db.set({
+ boardID: boardID,
+ threadID: threadID,
+ val: data
+ });
+ }
+ return ThreadWatcher.refresh();
+ }
+ }, {
+ type: 'head'
+ });
+ },
+ getAll: function() {
+ var all, boardID, data, threadID, threads, _ref;
+
+ all = [];
+ _ref = ThreadWatcher.db.data.boards;
+ for (boardID in _ref) {
+ threads = _ref[boardID];
+ if (Conf['Current Board'] && boardID !== g.BOARD.ID) {
+ continue;
+ }
+ for (threadID in threads) {
+ data = threads[threadID];
+ all.push({
+ boardID: boardID,
+ threadID: threadID,
+ data: data
+ });
+ }
+ }
+ return all;
+ },
+ makeLine: function(boardID, threadID, data) {
+ var div, fullID, href, link, x;
+
+ x = $.el('a', {
+ textContent: '×',
+ href: 'javascript:;'
+ });
+ $.on(x, 'click', ThreadWatcher.cb.rm);
+ if (data.isDead) {
+ href = Redirect.to('thread', {
+ boardID: boardID,
+ threadID: threadID
+ });
+ }
+ link = $.el('a', {
+ href: href || ("/" + boardID + "/res/" + threadID),
+ textContent: data.excerpt,
+ title: data.excerpt
+ });
+ div = $.el('div');
+ fullID = "" + boardID + "." + threadID;
+ div.dataset.fullID = fullID;
+ if (g.VIEW === 'thread' && fullID === ("" + g.BOARD + "." + g.THREADID)) {
+ $.addClass(div, 'current');
+ }
+ if (data.isDead) {
+ $.addClass(div, 'dead-thread');
+ }
+ $.add(div, [x, $.tn(' '), link]);
+ return div;
+ },
+ refresh: function() {
+ var boardID, data, list, nodes, refresher, thread, threadID, toggler, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
+
+ nodes = [];
+ _ref = ThreadWatcher.getAll();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ _ref1 = _ref[_i], boardID = _ref1.boardID, threadID = _ref1.threadID, data = _ref1.data;
+ nodes.push(ThreadWatcher.makeLine(boardID, threadID, data));
+ }
+ list = ThreadWatcher.list;
+ $.rmAll(list);
+ $.add(list, nodes);
+ _ref2 = g.BOARD.threads;
+ for (threadID in _ref2) {
+ thread = _ref2[threadID];
+ toggler = $('.watcher-toggler', thread.OP.nodes.post);
+ toggler.src = ThreadWatcher.db.get({
+ boardID: thread.board.ID,
+ threadID: threadID
+ }) ? Favicon["default"] : Favicon.empty;
+ }
+ _ref3 = ThreadWatcher.menu.refreshers;
+ for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
+ refresher = _ref3[_j];
+ refresher();
}
},
toggle: function(thread) {
- if (!$.hasClass($('.watch-thread-link', thread.OP.nodes.post), 'watched')) {
- return ThreadWatcher.watch(thread);
+ var boardID, threadID;
+
+ boardID = thread.board.ID;
+ threadID = thread.ID;
+ if (ThreadWatcher.db.get({
+ boardID: boardID,
+ threadID: threadID
+ })) {
+ return ThreadWatcher.rm(boardID, threadID);
} else {
- return ThreadWatcher.unwatch(thread.board, thread.ID);
+ return ThreadWatcher.add(thread);
}
},
- unwatch: function(board, threadID) {
- return $.get('WatchedThreads', {}, function(item) {
- var watched;
+ add: function(thread) {
+ var boardID, data, threadID;
- watched = item['WatchedThreads'];
- delete watched[board][threadID];
- if (!Object.keys(watched[board]).length) {
- delete watched[board];
+ data = {};
+ boardID = thread.board.ID;
+ threadID = thread.ID;
+ if (thread.isDead) {
+ if (Conf['Auto Prune'] && ThreadWatcher.db.get({
+ boardID: boardID,
+ threadID: threadID
+ })) {
+ ThreadWatcher.rm(boardID, threadID);
+ return;
}
- ThreadWatcher.refresh(watched);
- return $.set('WatchedThreads', watched);
+ data.isDead = true;
+ }
+ data.excerpt = Get.threadExcerpt(thread);
+ ThreadWatcher.db.set({
+ boardID: boardID,
+ threadID: threadID,
+ val: data
});
+ return ThreadWatcher.refresh();
},
- watch: function(thread) {
- return $.get('WatchedThreads', {}, function(item) {
- var watched, _name;
-
- watched = item['WatchedThreads'];
- watched[_name = thread.board] || (watched[_name] = {});
- watched[thread.board][thread] = {
- href: "/" + thread.board + "/res/" + thread,
- textContent: Get.threadExcerpt(thread)
- };
- ThreadWatcher.refresh(watched);
- return $.set('WatchedThreads', watched);
+ rm: function(boardID, threadID) {
+ ThreadWatcher.db["delete"]({
+ boardID: boardID,
+ threadID: threadID
});
+ return ThreadWatcher.refresh();
+ },
+ convert: function(oldFormat) {
+ var boardID, data, newFormat, threadID, threads;
+
+ newFormat = {};
+ for (boardID in oldFormat) {
+ threads = oldFormat[boardID];
+ for (threadID in threads) {
+ data = threads[threadID];
+ (newFormat[boardID] || (newFormat[boardID] = {}))[threadID] = {
+ excerpt: data.textContent
+ };
+ }
+ }
+ return newFormat;
+ },
+ menu: {
+ refreshers: [],
+ init: function() {
+ var menu;
+
+ if (!Conf['Thread Watcher']) {
+ return;
+ }
+ 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();
+ },
+ addHeaderMenuEntry: function() {
+ var entryEl;
+
+ if (g.VIEW !== 'thread') {
+ return;
+ }
+ entryEl = $.el('a', {
+ href: 'javascript:;'
+ });
+ $.event('AddMenuEntry', {
+ type: 'header',
+ el: entryEl,
+ order: 60
+ });
+ $.on(entryEl, 'click', function() {
+ return ThreadWatcher.toggle(g.threads["" + g.BOARD + "." + g.THREADID]);
+ });
+ return this.refreshers.push(function() {
+ var addClass, rmClass, text, _ref;
+
+ _ref = $('.current', ThreadWatcher.list) ? ['unwatch-thread', 'watch-thread', 'Unwatch thread'] : ['watch-thread', 'unwatch-thread', 'Watch thread'], addClass = _ref[0], rmClass = _ref[1], text = _ref[2];
+ $.addClass(entryEl, addClass);
+ $.rmClass(entryEl, rmClass);
+ return entryEl.textContent = text;
+ });
+ },
+ addMenuEntries: function() {
+ var cb, conf, entries, entry, name, refresh, subEntries, _i, _len, _ref, _ref1, _results;
+
+ entries = [];
+ entries.push({
+ cb: ThreadWatcher.cb.openAll,
+ entry: {
+ type: 'thread watcher',
+ el: $.el('a', {
+ textContent: 'Open all threads'
+ })
+ },
+ refresh: function() {
+ return (ThreadWatcher.list.firstElementChild ? $.rmClass : $.addClass)(this.el, 'disabled');
+ }
+ });
+ entries.push({
+ cb: ThreadWatcher.cb.checkThreads,
+ entry: {
+ type: 'thread watcher',
+ 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: {
+ type: 'thread watcher',
+ el: $.el('a', {
+ textContent: 'Prune 404\'d threads'
+ })
+ },
+ refresh: function() {
+ return ($('.dead-thread', ThreadWatcher.list) ? $.rmClass : $.addClass)(this.el, 'disabled');
+ }
+ });
+ subEntries = [];
+ _ref = Config.threadWatcher;
+ for (name in _ref) {
+ conf = _ref[name];
+ subEntries.push(this.createSubEntry(name, conf[1]));
+ }
+ entries.push({
+ entry: {
+ type: 'thread watcher',
+ el: $.el('span', {
+ textContent: 'Settings'
+ }),
+ subEntries: subEntries
+ }
+ });
+ _results = [];
+ for (_i = 0, _len = entries.length; _i < _len; _i++) {
+ _ref1 = entries[_i], entry = _ref1.entry, cb = _ref1.cb, refresh = _ref1.refresh;
+ if (entry.el.nodeName === 'A') {
+ entry.el.href = 'javascript:;';
+ }
+ if (cb) {
+ $.on(entry.el, 'click', cb);
+ }
+ if (refresh) {
+ this.refreshers.push(refresh.bind(entry));
+ }
+ _results.push($.event('AddMenuEntry', entry));
+ }
+ return _results;
+ },
+ createSubEntry: function(name, desc) {
+ var entry, input;
+
+ entry = {
+ type: 'thread watcher',
+ el: $.el('label', {
+ innerHTML: " " + name,
+ title: desc
+ })
+ };
+ input = entry.el.firstElementChild;
+ input.checked = Conf[name];
+ $.on(input, 'change', $.cb.checked);
+ if (name === 'Current Board') {
+ $.on(input, 'change', ThreadWatcher.refresh);
+ }
+ return entry;
+ }
}
};
@@ -8614,11 +8956,8 @@
},
callbacks: [],
cb: function(e) {
- var post;
-
e.preventDefault();
- post = Get.postFromNode(this);
- return ExpandComment.expand(post);
+ return ExpandComment.expand(Get.postFromNode(this));
},
expand: function(post) {
var a;
@@ -8681,6 +9020,12 @@
}
quote.href = "/" + post.board + "/res/" + href;
}
+ Build.capcodeReplies({
+ boardID: post.board.ID,
+ threadID: post.thread.ID,
+ bq: clone,
+ capcodeReplies: postObj.capcode_replies
+ });
post.nodes.shortComment = comment;
$.replace(comment, clone);
post.nodes.comment = post.nodes.longComment = clone;
@@ -9723,7 +10068,8 @@
$.on(d, '4chanXInitFinished', Settings.open);
}
return $.set({
- lastchecked: Date.now(),
+ archives: Conf['archives'],
+ lastarchivecheck: now,
previousversion: g.VERSION
});
});
@@ -9918,7 +10264,6 @@
version: g.VERSION,
date: now
};
- Conf['WatchedThreads'] = {};
for (_i = 0, _len = DataBoards.length; _i < _len; _i++) {
db = DataBoards[_i];
Conf[db] = {
@@ -10051,14 +10396,13 @@
});
}
}
- data.Conf.WatchedThreads = data.WatchedThreads;
- } else if (version[0] === '3') {
- data = Settings.convertSettings(data, {
- 'Reply Hiding': 'Reply Hiding Buttons',
- 'Thread Hiding': 'Thread Hiding Buttons',
- 'Bottom header': 'Bottom Header',
- 'Unread Tab Icon': 'Unread Favicon'
- });
+ data.Conf['WatchedThreads'] = data.WatchedThreads;
+ }
+ if (data.Conf['WatchedThreads']) {
+ data.Conf['watchedThreads'] = {
+ boards: ThreadWatcher.convert(data.Conf['WatchedThreads'])
+ };
+ delete data.Conf['WatchedThreads'];
}
return $.set(data.Conf);
},
@@ -10505,6 +10849,7 @@
'Thread Stats': ThreadStats,
'Thread Updater': ThreadUpdater,
'Thread Watcher': ThreadWatcher,
+ 'Thread Watcher (Menu)': ThreadWatcher.menu,
'Index Navigation': Nav,
'Keybinds': Keybinds,
'Show Dice Roll': Dice
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 000000000..32819ef69
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,929 @@
+/* General */
+.dialog {
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
+ border: 1px solid;
+ display: block;
+ padding: 0;
+}
+.field {
+ background-color: #FFF;
+ border: 1px solid #CCC;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ color: #333;
+ font-family: inherit;
+ font-size: 13px;
+ margin: 0;
+ padding: 2px 4px 3px;
+ outline: none;
+ transition: color .25s, border-color .25s, -webkit-flex .25s;
+ transition: color .25s, border-color .25s, flex .25s;
+}
+.field::-moz-placeholder,
+.field:hover::-moz-placeholder {
+ color: #AAA !important;
+}
+.field:hover {
+ border-color: #999;
+}
+.field:hover, .field:focus {
+ color: #000;
+}
+.field[disabled] {
+ background-color: #F2F2F2;
+ color: #888;
+}
+.move {
+ cursor: move;
+}
+label, .watcher-toggler {
+ cursor: pointer;
+}
+a[href="javascript:;"] {
+ text-decoration: none;
+}
+.warning {
+ color: red;
+}
+
+/* 4chan style fixes */
+.opContainer, .op {
+ display: block !important;
+}
+.post {
+ overflow: visible !important;
+}
+[hidden] {
+ display: none !important;
+}
+
+/* fixed, z-index */
+#overlay,
+#qp, #ihover,
+#updater, #thread-stats,
+#navlinks, #header,
+#qr {
+ position: fixed;
+}
+#overlay {
+ z-index: 999;
+}
+#notifications {
+ z-index: 70;
+}
+#qp, #ihover {
+ z-index: 60;
+}
+#menu {
+ z-index: 50;
+}
+#navlinks, #updater, #thread-stats {
+ z-index: 40;
+}
+#qr {
+ z-index: 30;
+}
+#thread-watcher:hover {
+ z-index: 20;
+}
+#header {
+ z-index: 10;
+}
+#thread-watcher {
+ z-index: 5;
+}
+
+/* Header */
+:root.top-header body {
+ margin-top: 2em;
+}
+:root.bottom-header body {
+ margin-bottom: 2em;
+}
+:root.fourchan-x #navtopright,
+:root.fourchan-x #navbotright,
+:root.fourchan-x:not(.show-original-top-board-list) #boardNavDesktop,
+:root.fourchan-x:not(.show-original-bot-board-list) #boardNavDesktopFoot {
+ display: none !important;
+}
+#header {
+ right: 0;
+ left: 0;
+}
+#header.top {
+ top: 0;
+}
+#header.bottom {
+ bottom: 0;
+}
+#header-bar {
+ border-width: 0;
+ display: -webkit-flex;
+ display: flex;
+ padding: 3px 4px 4px;
+ position: relative;
+ transition: all .1s .05s ease-in-out;
+}
+#header.top #header-bar {
+ border-bottom-width: 1px;
+}
+#header.bottom #header-bar {
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, .15);
+ border-top-width: 1px;
+}
+#header.bottom .menu-button i {
+ border-top: none;
+ border-bottom: 6px solid;
+}
+#board-list {
+ -webkit-flex: 1;
+ flex: 1;
+ text-align: center;
+}
+#header-bar.autohide:not(:hover) {
+ box-shadow: none;
+ transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
+}
+#header.top #header-bar.autohide:not(:hover) {
+ margin-bottom: -1em;
+ -webkit-transform: translateY(-100%);
+ transform: translateY(-100%);
+}
+#header.bottom #header-bar.autohide:not(:hover) {
+ -webkit-transform: translateY(100%);
+ transform: translateY(100%);
+}
+#toggle-header-bar {
+ left: 0;
+ right: 0;
+ height: 10px;
+ position: absolute;
+}
+#header.top #toggle-header-bar {
+ cursor: n-resize;
+ bottom: -8px;
+}
+#header.bottom #toggle-header-bar {
+ cursor: s-resize;
+ top: -8px;
+}
+#header-bar.autohide:not(:hover) #toggle-header-bar,
+#toggle-header-bar:hover {
+ height: 18px;
+}
+#header.top #header-bar.autohide:not(:hover) #toggle-header-bar,
+#header.top #toggle-header-bar:hover {
+ bottom: -16px;
+}
+#header.bottom #header-bar.autohide:not(:hover) #toggle-header-bar,
+#header.bottom #toggle-header-bar:hover {
+ top: -16px;
+}
+#header.top #header-bar.autohide #toggle-header-bar {
+ cursor: s-resize;
+}
+#header.bottom #header-bar.autohide #toggle-header-bar {
+ cursor: n-resize;
+}
+#header-bar a:not(.entry) {
+ text-decoration: none;
+ padding: 1px;
+}
+#shortcuts:empty {
+ display: none;
+}
+.shortcut:not(:last-child)::after {
+ content: " / ";
+}
+.brackets-wrap::before {
+ content: "\\00a0[";
+}
+.brackets-wrap::after {
+ content: "]\\00a0";
+}
+.expand-all-shortcut {
+ opacity: .35;
+}
+
+/* Notifications */
+#notifications {
+ height: 0;
+ text-align: center;
+}
+#header.bottom #notifications {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+}
+.notification {
+ color: #FFF;
+ font-weight: 700;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, .5);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
+ border-radius: 2px;
+ margin: 1px auto;
+ width: 500px;
+ max-width: 100%;
+ position: relative;
+ transition: all .25s ease-in-out;
+}
+.notification.error {
+ background-color: hsla(0, 100%, 38%, .9);
+}
+.notification.warning {
+ background-color: hsla(36, 100%, 38%, .9);
+}
+.notification.info {
+ background-color: hsla(200, 100%, 38%, .9);
+}
+.notification.success {
+ background-color: hsla(104, 100%, 38%, .9);
+}
+.notification a {
+ color: white;
+}
+.notification > .close {
+ padding: 6px;
+ top: 0;
+ right: 0;
+ position: absolute;
+}
+.message {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ padding: 6px 20px;
+ max-height: 200px;
+ width: 100%;
+ overflow: auto;
+}
+
+/* Settings */
+:root.fourchan-x body {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+#overlay {
+ background-color: rgba(0, 0, 0, .5);
+ display: -webkit-flex;
+ display: flex;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+}
+#fourchanx-settings {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ box-shadow: 0 0 15px rgba(0, 0, 0, .15);
+ height: 600px;
+ max-height: 100%;
+ width: 900px;
+ max-width: 100%;
+ margin: auto;
+ padding: 3px;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+}
+#fourchanx-settings > nav {
+ display: -webkit-flex;
+ display: flex;
+ padding: 2px 2px 0;
+}
+#fourchanx-settings > nav a {
+ text-decoration: underline;
+}
+#fourchanx-settings > nav a.close {
+ text-decoration: none;
+ padding: 2px;
+}
+.sections-list {
+ -webkit-flex: 1;
+ flex: 1;
+}
+.tab-selected {
+ font-weight: 700;
+}
+.section-container {
+ -webkit-flex: 1;
+ flex: 1;
+ position: relative;
+}
+.section-container > section {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: auto;
+}
+.section-sauce ul,
+.section-rice ul {
+ list-style: none;
+ margin: 0;
+ padding: 8px;
+}
+.section-sauce li,
+.section-rice li {
+ padding-left: 4px;
+}
+.section-main label {
+ text-decoration: underline;
+}
+.section-filter ul,
+.section-qr ul {
+ padding: 0;
+}
+.section-filter li,
+.section-qr li {
+ margin: 10px 40px;
+}
+.section-filter textarea {
+ height: 500px;
+}
+.section-qr textarea {
+ height: 200px;
+}
+.section-sauce textarea {
+ height: 350px;
+}
+.section-rice .field[name="boardnav"] {
+ width: 100%;
+}
+.section-rice textarea {
+ height: 150px;
+}
+.section-archives table {
+ width: 100%;
+}
+.section-archives th:not(:first-child) {
+ width: 30%;
+}
+.section-archives td {
+ text-align: center;
+}
+.section-archives select {
+ width: 90%;
+}
+.section-keybinds .field {
+ font-family: monospace;
+}
+#fourchanx-settings fieldset {
+ border: 1px solid;
+ border-radius: 3px;
+}
+#fourchanx-settings legend {
+ font-weight: 700;
+}
+#fourchanx-settings textarea {
+ font-family: monospace;
+ min-width: 100%;
+ max-width: 100%;
+}
+#fourchanx-settings code {
+ color: #000;
+ background-color: #FFF;
+ padding: 0 2px;
+}
+.unscroll {
+ overflow: hidden;
+}
+
+/* Announcement Hiding */
+:root.hide-announcement #globalMessage,
+:root.hide-announcement-enabled #toggleMsgBtn {
+ display: none;
+}
+a.hide-announcement {
+ float: left;
+}
+
+/* Unread */
+#unread-line {
+ margin: 0;
+}
+
+/* Thread Updater */
+#updater:not(:hover) {
+ background: none;
+ border: none;
+ box-shadow: none;
+}
+#updater > .move {
+ padding: 0 3px;
+}
+#updater > div:last-child {
+ text-align: center;
+}
+#updater input[type=number] {
+ width: 4em;
+}
+#updater:not(:hover) > div:not(.move) {
+ display: none;
+}
+#updater input[type="button"] {
+ width: 100%;
+}
+.new {
+ color: limegreen;
+}
+
+/* Thread Watcher */
+#thread-watcher {
+ max-width: 200px;
+ min-width: 150px;
+ padding: 3px;
+ position: absolute;
+}
+#thread-watcher > div:first-child {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
+}
+#thread-watcher .move {
+ -webkit-flex: 1;
+ flex: 1;
+}
+#watcher-status:not(:empty)::before {
+ content: "(";
+}
+#watcher-status:not(:empty)::after {
+ content: ")";
+}
+#watched-threads:not(:hover) {
+ max-height: 150px;
+ overflow: hidden;
+}
+#watched-threads div {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+#watched-threads .current {
+ font-weight: 700;
+}
+#watched-threads a {
+ text-decoration: none;
+}
+#watched-threads .dead-thread a[title] {
+ text-decoration: line-through;
+}
+
+/* Thread Stats */
+#thread-stats {
+ background: none;
+ border: none;
+ box-shadow: none;
+}
+
+/* Quote */
+.deadlink {
+ text-decoration: none !important;
+}
+.backlink.deadlink:not(.forwardlink),
+.quotelink.deadlink:not(.forwardlink) {
+ text-decoration: underline !important;
+}
+.inlined {
+ opacity: .5;
+}
+#qp input, .forwarded {
+ display: none;
+}
+.quotelink.forwardlink,
+.backlink.forwardlink {
+ text-decoration: none;
+ border-bottom: 1px dashed;
+}
+.filtered {
+ text-decoration: underline line-through;
+}
+.inline {
+ border: 1px solid;
+ display: table;
+ margin: 2px 0;
+}
+.inline .post {
+ border: 0 !important;
+ background-color: transparent !important;
+ display: table !important;
+ margin: 0 !important;
+ padding: 1px 2px !important;
+}
+#qp > .opContainer::after {
+ content: '';
+ clear: both;
+ display: table;
+}
+#qp .post {
+ border: none;
+ margin: 0;
+ padding: 2px 2px 5px;
+}
+#qp img {
+ max-height: 80vh;
+ max-width: 50vw;
+}
+.qphl {
+ outline: 2px solid rgba(216, 94, 49, .7);
+}
+
+/* File */
+.fileText:hover .fntrunc,
+.fileText:not(:hover) .fnfull,
+.expanded-image > .post > .file > .fileThumb > img[data-md5],
+:not(.expanded-image) > .post > .file > .fileThumb > .full-image {
+ display: none;
+}
+.expanding {
+ opacity: .5;
+}
+.expanded-image {
+ clear: both;
+}
+.expanded-image > .op > .file::after {
+ content: '';
+ clear: both;
+ display: table;
+}
+:root.fit-height .full-image {
+ max-height: 100vh;
+}
+:root.fit-width .full-image {
+ max-width: 100%;
+}
+:root.gecko.fit-width .full-image {
+ width: 100%;
+}
+#ihover {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ max-height: 100%;
+ max-width: 75%;
+ padding-bottom: 16px;
+}
+
+/* Index/Reply Navigation */
+#navlinks {
+ font-size: 16px;
+ top: 25px;
+ right: 10px;
+}
+
+/* Filter */
+.opContainer.filter-highlight {
+ box-shadow: inset 5px 0 rgba(255, 0, 0, .5);
+}
+.filter-highlight > .reply {
+ box-shadow: -5px 0 rgba(255, 0, 0, .5);
+}
+
+/* Thread & Reply Hiding */
+.hide-thread-button,
+.hide-reply-button {
+ float: left;
+ margin-right: 2px;
+}
+.stub ~ * {
+ display: none !important;
+}
+.stub input {
+ display: inline-block;
+}
+
+/* QR */
+:root.hide-original-post-form #postForm,
+:root.hide-original-post-form .postingMode,
+:root.hide-original-post-form #togglePostForm,
+#qr.autohide:not(.has-focus):not(:hover) > form {
+ display: none;
+}
+#qr select, #dump-button, .remove, .captcha-img {
+ cursor: pointer;
+}
+#qr > div {
+ min-width: 300px;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
+}
+#qr .move {
+ -webkit-align-self: stretch;
+ align-self: stretch;
+ -webkit-flex: 1;
+ flex: 1;
+}
+#qr select {
+ margin: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border: none;
+ background: none;
+}
+#qr option {
+ color: #000;
+ background-color: #F7F7F7;
+}
+#qr .close {
+ padding: 0 3px;
+}
+#qr > form {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+}
+.persona {
+ display: -webkit-flex;
+ display: flex;
+}
+.persona .field {
+ -webkit-flex: 1;
+ flex: 1;
+}
+.persona .field:hover,
+.persona .field:focus {
+ -webkit-flex: 3;
+ flex: 3;
+}
+#dump-button {
+ background: linear-gradient(#EEE, #CCC);
+ border: 1px solid #CCC;
+ margin: 0;
+ padding: 2px 4px 3px;
+ outline: none;
+ width: 30px;
+}
+#dump-button:hover,
+#dump-button:focus {
+ background: linear-gradient(#FFF, #DDD);
+}
+#dump-button:active,
+.dump #dump-button:not(:hover):not(:focus) {
+ background: linear-gradient(#CCC, #DDD);
+}
+:root.gecko #dump-button {
+ padding: 0;
+}
+#qr:not(.dump) #dump-list-container {
+ display: none;
+}
+#dump-list-container {
+ height: 100px;
+ position: relative;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+#dump-list {
+ counter-reset: qrpreviews;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+}
+#dump-list:hover {
+ bottom: -12px;
+ overflow-x: auto;
+ z-index: 1;
+}
+#dump-list::-webkit-scrollbar {
+ height: 12px;
+}
+#dump-list::-webkit-scrollbar-thumb {
+ border: 1px solid;
+}
+.qr-preview {
+ background-position: 50% 20%;
+ background-size: cover;
+ border: 1px solid #808080;
+ color: #FFF !important;
+ font-size: 12px;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ cursor: move;
+ display: inline-block;
+ height: 92px;
+ width: 92px;
+ margin: 4px;
+ padding: 2px;
+ opacity: .6;
+ outline: none;
+ overflow: hidden;
+ position: relative;
+ text-shadow: 0 1px 1px #000;
+ transition: opacity .25s ease-in-out;
+ vertical-align: top;
+ white-space: pre;
+}
+.qr-preview:hover,
+.qr-preview:focus {
+ opacity: .9;
+ color: #FFF !important;
+}
+.qr-preview#selected {
+ opacity: 1;
+}
+.qr-preview::before {
+ counter-increment: qrpreviews;
+ content: counter(qrpreviews);
+ font-weight: 700;
+ text-shadow: 0 0 3px #000, 0 0 5px #000;
+ position: absolute;
+ top: 3px;
+ right: 3px;
+}
+.qr-preview.drag {
+ border-color: red;
+ border-style: dashed;
+}
+.qr-preview.over {
+ border-color: #FFF;
+ border-style: dashed;
+}
+.remove {
+ color: #E00 !important;
+ font-weight: 700;
+ padding: 3px;
+}
+.remove:hover::after {
+ content: ' Remove';
+}
+.qr-preview > label {
+ background: rgba(0, 0, 0, .5);
+ right: 0;
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ text-align: center;
+}
+.qr-preview > label > input {
+ margin: 1px 0;
+ vertical-align: bottom;
+}
+#add-post {
+ display: inline-block;
+ font-size: 30px;
+ height: 30px;
+ width: 30px;
+ line-height: 1;
+ text-align: center;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ z-index: 1;
+}
+#qr textarea {
+ min-height: 160px;
+ min-width: 100%;
+ display: block;
+}
+#qr.has-captcha textarea {
+ min-height: 120px;
+}
+.textarea {
+ position: relative;
+}
+#char-count {
+ color: #000;
+ background: hsla(0, 0%, 100%, .5);
+ font-size: 8pt;
+ position: absolute;
+ bottom: 1px;
+ right: 1px;
+ pointer-events: none;
+}
+#char-count.warning {
+ color: red;
+}
+.captcha-img {
+ background: #FFF;
+ outline: 1px solid #CCC;
+ outline-offset: -1px;
+}
+.captcha-img > img {
+ display: block;
+ height: 57px;
+ width: 300px;
+}
+#file-n-submit > input {
+ margin: 0;
+}
+#file-n-submit.has-file #qr-no-file {
+ visibility: hidden;
+}
+#file-n-submit:not(.has-file) #qr-filename,
+#file-n-submit:not(.has-file) #qr-file-spoiler,
+#file-n-submit:not(.has-file) #qr-filerm {
+ display: none;
+}
+#file-n-submit {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ align-items: center;
+}
+#qr-no-file, #qr-filename-container {
+ -webkit-flex: 1;
+ flex: 1;
+}
+#qr-filename-container {
+ cursor: default;
+ position: relative;
+ margin-left: 2px;
+}
+#qr-filename {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+#qr-filerm {
+ padding: 0 2px;
+}
+#file-n-submit > #qr-file-spoiler {
+ margin: 0 2px;
+}
+#file-n-submit input[type='submit'] {
+ min-width: 40px;
+ -webkit-order: 1;
+ order: 1;
+}
+
+/* Menu */
+.menu-button {
+ display: inline-block;
+ position: relative;
+}
+.menu-button i {
+ border-top: 6px solid;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent;
+ display: inline-block;
+ margin: 2px;
+ vertical-align: middle;
+}
+#menu {
+ border-bottom: 0;
+ display: -webkit-flex;
+ display: flex;
+ margin: 2px 0;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ position: absolute;
+ outline: none;
+}
+.entry {
+ cursor: pointer;
+ outline: none;
+ padding: 3px 7px;
+ position: relative;
+ text-decoration: none;
+ white-space: nowrap;
+}
+.entry.disabled {
+ color: graytext !important;
+}
+.entry.has-submenu {
+ padding-right: 20px;
+}
+.has-submenu::after {
+ content: '';
+ border-left: 6px solid;
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ display: inline-block;
+ margin: 4px;
+ position: absolute;
+ right: 3px;
+}
+.has-submenu:not(.focused) > .submenu {
+ display: none;
+}
+.submenu {
+ border-bottom: 0;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ position: absolute;
+ margin: -1px 0;
+}
+.entry input {
+ margin: 0;
+}
diff --git a/html/Monitoring/ThreadWatcher.html b/html/Monitoring/ThreadWatcher.html
new file mode 100644
index 000000000..8ea19b356
--- /dev/null
+++ b/html/Monitoring/ThreadWatcher.html
@@ -0,0 +1,5 @@
+
+ Thread Watcher
+
+
+
diff --git a/json/archives.json b/json/archives.json
index 95ac1a37a..647889b55 100644
--- a/json/archives.json
+++ b/json/archives.json
@@ -50,8 +50,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["adv", "asp", "cm", "e", "i", "lgbt", "n", "o", "p", "pol", "s", "s4s", "t", "trv", "y"],
- "files": ["adv", "asp", "cm", "e", "i", "lgbt", "n", "o", "p", "s", "s4s", "t", "trv", "y"]
+ "boards": ["adv", "asp", "cm", "d", "e", "i", "lgbt", "n", "o", "p", "pol", "s", "s4s", "t", "trv", "y"],
+ "files": ["cm", "d", "e", "i", "n", "o", "p", "s", "trv", "y"]
}, {
"uid": 12,
"name": "fap archive",
diff --git a/src/General/Build.coffee b/src/General/Build.coffee
index cd4f73e01..6a1683a38 100644
--- a/src/General/Build.coffee
+++ b/src/General/Build.coffee
@@ -27,7 +27,7 @@ Build =
date: data.now
dateUTC: data.time
comment: data.com
- capReps: data.capcode_replies
+ capcodeReplies: data.capcode_replies
# thread status
isSticky: !!data.sticky
isClosed: !!data.closed
@@ -59,7 +59,7 @@ Build =
postID, threadID, boardID
name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC
isSticky, isClosed
- comment, capReps
+ comment, capcodeReplies
file
} = o
isOP = postID is threadID
@@ -191,26 +191,6 @@ Build =
else
''
- capcodeReplies = ''
- if capReps
- generateCapcodeReplies = (capcodeType, array) ->
- "#{
- switch capcodeType
- when 'admin'
- 'Administrator'
- when 'mod'
- 'Moderator'
- when 'developer'
- 'Developer'
- } Repl#{if array.length > 1 then 'ies' else 'y'}: #{
- array.map (ID) ->
- ">>#{ID}"
- .join ' '
- }
"
- for capcodeType, array of capReps
- capcodeReplies += generateCapcodeReplies capcodeType, array
- capcodeReplies = "
#{capcodeReplies}"
-
container = $.el 'div',
id: "pc#{postID}"
className: "postContainer #{if isOP then 'op' else 'reply'}Container"
@@ -221,4 +201,36 @@ Build =
continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
+ Build.capcodeReplies {boardID, threadID, root: container, capcodeReplies}
+
container
+
+ capcodeReplies: ({boardID, threadID, bq, root, capcodeReplies}) ->
+ return unless capcodeReplies
+
+ generateCapcodeReplies = (capcodeType, array) ->
+ "#{
+ switch capcodeType
+ when 'admin'
+ 'Administrator'
+ when 'mod'
+ 'Moderator'
+ when 'developer'
+ 'Developer'
+ } Repl#{if array.length > 1 then 'ies' else 'y'}: #{
+ array.map (ID) ->
+ ">>#{ID}"
+ .join ' '
+ }
"
+ html = []
+ for capcodeType, array of capcodeReplies
+ html.push generateCapcodeReplies capcodeType, array
+
+ bq or= $ 'blockquote', root
+ $.add bq, [
+ $.el 'br'
+ $.el 'br'
+ $.el 'span',
+ className: 'capcodeReplies'
+ innerHTML: html.join ''
+ ]
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index 819779b89..83c2b3df9 100644
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -57,12 +57,6 @@ Config =
true
'Show dice that were entered into the email field.'
]
- <% if (type !== 'crx') { %>
- 'Check for Updates': [
- true
- 'Check for updated versions of <%= meta.name %>.'
- ]
- <% } %>
'Show Updated Notifications': [
true
'Show notifications when 4chan X is successfully updated.'
@@ -255,14 +249,6 @@ Config =
true
'Adds a shortcut for the thread watcher, hides the watcher by default, and makes it scroll with the page.'
]
- 'Auto Watch': [
- true
- 'Automatically watch threads you start.'
- ]
- 'Auto Watch Reply': [
- false
- 'Automatically watch threads you reply to.'
- ]
'Posting':
'Quick Reply': [
@@ -399,7 +385,25 @@ Config =
false
'Advance to next post when contracting an expanded image.'
]
-
+
+ threadWatcher:
+ 'Current Board': [
+ false
+ 'Only show watched threads from the current board.'
+ ]
+ 'Auto Watch': [
+ true
+ 'Automatically watch threads you start.'
+ ]
+ 'Auto Watch Reply': [
+ false
+ 'Automatically watch threads you reply to.'
+ ]
+ 'Auto Prune': [
+ false
+ 'Automatically prune 404\'d threads.'
+ ]
+
filter:
name: """
# Filter any namefags:
diff --git a/src/General/Header.coffee b/src/General/Header.coffee
index d08b8cd99..947f084e4 100644
--- a/src/General/Header.coffee
+++ b/src/General/Header.coffee
@@ -109,7 +109,6 @@ Header =
fourchannav = $.id 'boardNavDesktop'
if a = $ "a[href*='/#{g.BOARD}/']", fourchannav
a.className = 'current'
-
boardList = $.el 'span',
id: 'board-list'
innerHTML: " - #{fourchannav.innerHTML}"
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index 86b92e391..f79be109b 100644
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -126,6 +126,7 @@ Main =
'Thread Stats': ThreadStats
'Thread Updater': ThreadUpdater
'Thread Watcher': ThreadWatcher
+ 'Thread Watcher (Menu)': ThreadWatcher.menu
'Index Navigation': Nav
'Keybinds': Keybinds
'Show Dice Roll': Dice
@@ -205,9 +206,6 @@ Main =
Main.callbackNodes Thread, threads
Main.callbackNodesDB Post, posts, ->
$.event '4chanXInitFinished'
- <% if (type !== 'crx') { %>
- Main.checkUpdate()
- <% } %>
if styleSelector = $.id 'styleSelector'
passLink = $.el 'a',
@@ -227,9 +225,6 @@ Main =
new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
$.event '4chanXInitFinished'
- <% if (type !== 'crx') { %>
- Main.checkUpdate()
- <% } %>
callbackNodes: (klass, nodes) ->
# get the nodes' length only once
@@ -303,27 +298,6 @@ Main =
obj.callback.isAddon = true
Klass::callbacks.push obj.callback
- <% if (type !== 'crx') { %>
- message: (e) ->
- {version} = e.data
- if version and version isnt g.VERSION
- el = $.el 'span',
- innerHTML: "Update: <%= meta.name %> v#{version} is out, get it target=_blank>here."
- new Notification 'info', el, 120
-
- checkUpdate: ->
- return unless Conf['Check for Updates'] and Main.isThisPageLegit()
- now = Date.now()
- $.get 'lastchecked', 0, ({lastchecked}) ->
- if (lastchecked > now - $.DAY)
- return
- $.ready ->
- $.on window, 'message', Main.message
- $.set 'lastchecked', now
- $.add d.head, $.el 'script',
- src: '<%= meta.repo %>raw/<%= meta.mainBranch %>/latest.js'
- <% } %>
-
handleErrors: (errors) ->
unless errors instanceof Array
error = errors
diff --git a/src/General/Settings.coffee b/src/General/Settings.coffee
index 8af3c568f..eaa4e908a 100644
--- a/src/General/Settings.coffee
+++ b/src/General/Settings.coffee
@@ -21,7 +21,8 @@ Settings =
else
$.on d, '4chanXInitFinished', Settings.open
$.set
- lastchecked: Date.now()
+ archives: Conf['archives']
+ lastarchivecheck: now
previousversion: g.VERSION
Settings.addSection 'Main', Settings.main
@@ -153,7 +154,6 @@ Settings =
data =
version: g.VERSION
date: now
- Conf['WatchedThreads'] = {}
for db in DataBoards
Conf[db] = boards: {}
# Make sure to export the most recent data.
@@ -265,13 +265,10 @@ Settings =
for key, val of Config.hotkeys when key of data.Conf
data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) ->
"Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}"
- data.Conf.WatchedThreads = data.WatchedThreads
- else if version[0] is '3'
- data = Settings.convertSettings data,
- 'Reply Hiding': 'Reply Hiding Buttons'
- 'Thread Hiding': 'Thread Hiding Buttons'
- 'Bottom header': 'Bottom Header'
- 'Unread Tab Icon': 'Unread Favicon'
+ data.Conf['WatchedThreads'] = data.WatchedThreads
+ if data.Conf['WatchedThreads']
+ data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
+ delete data.Conf['WatchedThreads']
$.set data.Conf
convertSettings: (data, map) ->
for prevKey, newKey of map
diff --git a/src/General/lib/$.coffee b/src/General/lib/$.coffee
index 238c79a0b..295bc41cc 100644
--- a/src/General/lib/$.coffee
+++ b/src/General/lib/$.coffee
@@ -57,18 +57,23 @@ $.extend = (object, properties) ->
object[key] = val
return
-$.ajax = (url, options, extra={}) ->
- {type, headers, upCallbacks, form, sync} = extra
- r = new XMLHttpRequest()
- r.overrideMimeType 'text/html'
- type or= form and 'post' or 'get'
- r.open type, url, !sync
- for key, val of headers
- r.setRequestHeader key, val
- $.extend r, options
- $.extend r.upload, upCallbacks
- r.send form
- r
+$.ajax = do ->
+ # Status Code 304: Not modified
+ # With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
+ # This saves a lot of bandwidth and CPU time for both the users and the servers.
+ lastModified = {}
+ (url, options, extra={}) ->
+ {type, whenModified, upCallbacks, form, sync} = extra
+ r = new XMLHttpRequest()
+ type or= form and 'post' or 'get'
+ r.open type, url, !sync
+ if whenModified
+ r.setRequestHeader 'If-Modified-Since', lastModified[url] or '0'
+ $.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
+ $.extend r, options
+ $.extend r.upload, upCallbacks
+ r.send form
+ r
$.cache = do ->
reqs = {}
diff --git a/src/General/lib/databoard.class b/src/General/lib/databoard.class
index 80c190f17..e1f58ed47 100644
--- a/src/General/lib/databoard.class
+++ b/src/General/lib/databoard.class
@@ -1,10 +1,10 @@
-DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts']
+DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
class DataBoard
- constructor: (@key, sync) ->
+ constructor: (@key, sync, dontClean) ->
@data = Conf[key]
$.sync key, @onSync.bind @
- @clean()
+ @clean() unless dontClean
return unless sync
# Chrome also fires the onChanged callback on the current tab,
# so we only start syncing when we're ready.
@@ -13,6 +13,9 @@ class DataBoard
@sync = sync
$.on d, '4chanXInitFinished', init
+ save: ->
+ $.set @key, @data
+
delete: ({boardID, threadID, postID}) ->
if postID
delete @data.boards[boardID][threadID][postID]
@@ -22,7 +25,7 @@ class DataBoard
@deleteIfEmpty {boardID}
else
delete @data.boards[boardID]
- $.set @key, @data
+ @save()
deleteIfEmpty: ({boardID, threadID}) ->
if threadID
@@ -39,7 +42,7 @@ class DataBoard
(@data.boards[boardID] or= {})[threadID] = val
else
@data.boards[boardID] = val
- $.set @key, @data
+ @save()
get: ({boardID, threadID, postID, defaultValue}) ->
if board = @data.boards[boardID]
@@ -67,8 +70,7 @@ class DataBoard
@data.lastChecked = now
for boardID of @data.boards
@ajaxClean boardID
-
- $.set @key, @data
+ @save()
ajaxClean: (boardID) ->
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
@@ -84,7 +86,7 @@ class DataBoard
threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads
@deleteIfEmpty {boardID}
- $.set @key, @data
+ @save()
onSync: (data) ->
@data = data or boards: {}
diff --git a/src/General/lib/polyfill.coffee b/src/General/lib/polyfill.coffee
index 5d9ddc124..3cec1905a 100644
--- a/src/General/lib/polyfill.coffee
+++ b/src/General/lib/polyfill.coffee
@@ -1,6 +1,16 @@
Polyfill =
init: ->
+ Polyfill.toBlob()
Polyfill.visibility()
+ toBlob: ->
+ HTMLCanvasElement::toBlob or= (cb) ->
+ data = atob @toDataURL()[22..]
+ # DataUrl to Binary code from Aeosynth's 4chan X repo
+ l = data.length
+ ui8a = new Uint8Array l
+ for i in [0...l]
+ ui8a[i] = data.charCodeAt i
+ cb new Blob [ui8a], type: 'image/png'
visibility: ->
# page visibility API
return unless 'webkitHidden' of document
diff --git a/src/Images/ImageExpand.coffee b/src/Images/ImageExpand.coffee
index a02e5ec31..3d7751853 100644
--- a/src/Images/ImageExpand.coffee
+++ b/src/Images/ImageExpand.coffee
@@ -169,8 +169,8 @@ ImageExpand =
{createSubEntry} = ImageExpand.menu
subEntries = []
- for key, conf of Config.imageExpansion
- subEntries.push createSubEntry key, conf
+ for name, conf of Config.imageExpansion
+ subEntries.push createSubEntry name, conf[1]
$.event 'AddMenuEntry',
type: 'header'
@@ -178,17 +178,16 @@ ImageExpand =
order: 105
subEntries: subEntries
- createSubEntry: (type, config) ->
+ createSubEntry: (name, desc) ->
label = $.el 'label',
- innerHTML: " #{type}"
+ innerHTML: " #{name}"
+ title: desc
input = label.firstElementChild
- if type in ['Fit width', 'Fit height']
+ if name in ['Fit width', 'Fit height']
$.on input, 'change', ImageExpand.cb.setFitness
- if config
- label.title = config[1]
- input.checked = Conf[type]
- $.event 'change', null, input
- $.on input, 'change', $.cb.checked
+ input.checked = Conf[name]
+ $.event 'change', null, input
+ $.on input, 'change', $.cb.checked
el: label
menuToggle: (e) ->
diff --git a/src/Miscellaneous/ExpandComment.coffee b/src/Miscellaneous/ExpandComment.coffee
index d3499eaea..ee7451dde 100644
--- a/src/Miscellaneous/ExpandComment.coffee
+++ b/src/Miscellaneous/ExpandComment.coffee
@@ -16,8 +16,7 @@ ExpandComment =
callbacks: []
cb: (e) ->
e.preventDefault()
- post = Get.postFromNode @
- ExpandComment.expand post
+ ExpandComment.expand Get.postFromNode @
expand: (post) ->
if post.nodes.longComment and !post.nodes.longComment.parentNode
$.replace post.nodes.shortComment, post.nodes.longComment
@@ -55,6 +54,11 @@ ExpandComment =
href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
+ Build.capcodeReplies
+ boardID: post.board.ID
+ threadID: post.thread.ID
+ bq: clone
+ capcodeReplies: postObj.capcode_replies
post.nodes.shortComment = comment
$.replace comment, clone
post.nodes.comment = post.nodes.longComment = clone
diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee
index a8002a575..452caf4cd 100644
--- a/src/Monitoring/ThreadStats.coffee
+++ b/src/Monitoring/ThreadStats.coffee
@@ -18,7 +18,6 @@ ThreadStats =
@postCountEl = $ '#post-count', sc
@fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc
- @lastModified = '0'
Thread::callbacks.push
name: 'Thread Stats'
@@ -55,12 +54,10 @@ ThreadStats =
return
setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
$.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
- headers: 'If-Modified-Since': ThreadStats.lastModified
+ whenModified: true
onThreadsLoad: ->
- return if !Conf["Page Count in Stats"]
- ThreadStats.lastModified = @getResponseHeader 'Last-Modified'
- return if @status isnt 200
+ return unless Conf["Page Count in Stats"] and @status is 200
pages = JSON.parse @response
for page in pages
for thread in page.threads
diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee
index ed4255104..2c245cba3 100644
--- a/src/Monitoring/ThreadUpdater.coffee
+++ b/src/Monitoring/ThreadUpdater.coffee
@@ -64,7 +64,6 @@ ThreadUpdater =
ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]
ThreadUpdater.outdateCount = 0
- ThreadUpdater.lastModified = '0'
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
@@ -136,9 +135,7 @@ ThreadUpdater =
when 200
g.DEAD = false
ThreadUpdater.parse JSON.parse(req.response).posts
- ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified'
- if Conf['Auto Update']
- ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
+ ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
when 404
g.DEAD = true
ThreadUpdater.set 'timer', null
@@ -149,14 +146,8 @@ ThreadUpdater =
404: true
thread: ThreadUpdater.thread
else
- if Conf['Auto Update']
- ThreadUpdater.outdateCount++
- ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
- ###
- Status Code 304: Not modified
- By sending the `If-Modified-Since` header we get a proper status code, and no response.
- This saves bandwidth for both the user and the servers and avoid unnecessary computation.
- ###
+ ThreadUpdater.outdateCount++
+ ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
[text, klass] = if req.status is 304
[null, null]
else
@@ -218,7 +209,7 @@ ThreadUpdater =
ThreadUpdater.req.abort()
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
- headers: 'If-Modified-Since': ThreadUpdater.lastModified
+ whenModified: true
updateThreadStatus: (title, OP) ->
titleLC = title.toLowerCase()
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index a2b5fc3c7..669d63619 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -1,113 +1,286 @@
ThreadWatcher =
init: ->
- return unless Conf['Thread Watcher']
- @shortcut = sc = $.el 'a',
- textContent: 'Watcher'
- id: 'watcher-link'
- href: 'javascript:;'
- className: 'disabled'
+ return if !Conf['Thread Watcher']
- @dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;',
- ''
+ @db = new DataBoard 'watchedThreads', @refresh, true
+ @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """
+ <%= grunt.file.read('html/Monitoring/ThreadWatcher.html').replace(/>\s+<').trim() %>
+ """
+ @status = $ '#watcher-status', @dialog
+ @list = @dialog.lastElementChild
$.on d, 'QRPostSuccessful', @cb.post
- $.sync 'WatchedThreads', @refresh
- $.on sc, 'click', @toggleWatcher
- $.on $('.move>.close', ThreadWatcher.dialog), 'click', @toggleWatcher
+ $.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
+ $.on d, '4chanXInitFinished', @ready
- if Conf['Toggleable Thread Watcher']
- Header.addShortcut sc
- $.addClass doc, 'fixed-watcher'
+ now = Date.now()
+ if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
+ @db.data.lastChecked = now
+ ThreadWatcher.fetchAllStatus()
+ @db.save()
- $.ready ->
- ThreadWatcher.refresh()
- $.add d.body, ThreadWatcher.dialog
- if Conf['Toggleable Thread Watcher']
- ThreadWatcher.dialog.hidden = true
+ # XXX tmp conversion from old to new format
+ $.get 'WatchedThreads', null, ({WatchedThreads}) ->
+ return unless WatchedThreads
+ for boardID, threads of ThreadWatcher.convert WatchedThreads
+ for threadID, data of threads
+ ThreadWatcher.db.set {boardID, threadID, val: data}
+ $.delete 'WatchedThreads'
Thread::callbacks.push
name: 'Thread Watcher'
cb: @node
node: ->
- favicon = $.el 'a',
- className: 'watch-thread-link'
- href: 'javascript:;'
- $.on favicon, 'click', ThreadWatcher.cb.toggle
- $.before $('input', @OP.nodes.post), favicon
- return if g.VIEW isnt 'thread'
- $.get 'AutoWatch', 0, (item) =>
- return if item['AutoWatch'] isnt @ID
- ThreadWatcher.watch @
+ toggler = $.el 'img',
+ className: 'watcher-toggler'
+ $.on toggler, 'click', ThreadWatcher.cb.toggle
+ $.before $('input', @OP.nodes.post), toggler
+
+ ready: ->
+ $.off d, '4chanXInitFinished', ThreadWatcher.ready
+ return unless Main.isThisPageLegit()
+ ThreadWatcher.refresh()
+ $.add d.body, ThreadWatcher.dialog
+
+ return unless Conf['Auto Watch']
+ $.get 'AutoWatch', 0, ({AutoWatch}) ->
+ return unless thread = g.BOARD.threads[AutoWatch]
+ ThreadWatcher.add thread
$.delete 'AutoWatch'
- refresh: (watched) ->
- unless watched
- $.get 'WatchedThreads', {}, ({WatchedThreads}) ->
- ThreadWatcher.refresh WatchedThreads
- return
- nodes = [$('.move', ThreadWatcher.dialog)]
- for board of watched
- for id, props of watched[board]
- x = $.el 'a',
- textContent: '×'
- className: 'close'
- href: 'javascript:;'
- $.on x, 'click', ThreadWatcher.cb.x
- link = $.el 'a', props
- link.title = link.textContent
-
- div = $.el 'div'
- $.add div, [x, $.tn(' '), link]
- nodes.push div
-
- $.rmAll ThreadWatcher.dialog
- $.add ThreadWatcher.dialog, nodes
-
- watched = watched[g.BOARD] or {}
- for ID, thread of g.BOARD.threads
- favicon = $ '.watch-thread-link', thread.OP.nodes.post
- $[if ID of watched then 'addClass' else 'rmClass'] favicon, 'watched'
- return
-
toggleWatcher: ->
$.toggleClass ThreadWatcher.shortcut, 'disabled'
ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden
cb:
+ openAll: ->
+ return if $.hasClass @, 'disabled'
+ 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
+ delete ThreadWatcher.db.data.boards[boardID][threadID]
+ ThreadWatcher.db.deleteIfEmpty {boardID}
+ ThreadWatcher.db.save()
+ ThreadWatcher.refresh()
+ $.event 'CloseMenu'
toggle: ->
ThreadWatcher.toggle Get.postFromNode(@).thread
- x: ->
- thread = @nextElementSibling.pathname.split '/'
- ThreadWatcher.unwatch thread[1], thread[3]
+ rm: ->
+ [boardID, threadID] = @parentNode.dataset.fullID.split '.'
+ ThreadWatcher.rm boardID, +threadID
post: (e) ->
{board, postID, threadID} = e.detail
if postID is threadID
if Conf['Auto Watch']
$.set 'AutoWatch', threadID
else if Conf['Auto Watch Reply']
- ThreadWatcher.watch board.threads[threadID]
+ ThreadWatcher.add board.threads[threadID]
+ threadUpdate: (e) ->
+ {thread} = e.detail
+ return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
+ # Update 404 status.
+ ThreadWatcher.add thread
+
+ fetchCount:
+ fetched: 0
+ fetching: 0
+ fetchAllStatus: ->
+ ThreadWatcher.status.textContent = '...'
+ for thread in ThreadWatcher.getAll()
+ ThreadWatcher.fetchStatus thread
+ return
+ fetchStatus: ({boardID, threadID, data}) ->
+ return if data.isDead
+ {fetchCount} = ThreadWatcher
+ fetchCount.fetching++
+ $.ajax "//api.4chan.org/#{boardID}/res/#{threadID}.json",
+ onloadend: ->
+ fetchCount.fetched++
+ if fetchCount.fetched is fetchCount.fetching
+ fetchCount.fetched = 0
+ fetchCount.fetching = 0
+ status = ''
+ else
+ status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%"
+ ThreadWatcher.status.textContent = status
+ return if @status isnt 404
+ if Conf['Auto Prune']
+ ThreadWatcher.rm boardID, threadID
+ else
+ data.isDead = true
+ ThreadWatcher.db.set {boardID, threadID, val: data}
+ ThreadWatcher.refresh()
+ ,
+ type: 'head'
+
+ getAll: ->
+ all = []
+ for boardID, threads of ThreadWatcher.db.data.boards
+ if Conf['Current Board'] and boardID isnt g.BOARD.ID
+ continue
+ for threadID, data of threads
+ all.push {boardID, threadID, data}
+ all
+
+ makeLine: (boardID, threadID, data) ->
+ x = $.el 'a',
+ textContent: '×'
+ href: 'javascript:;'
+ $.on x, 'click', ThreadWatcher.cb.rm
+
+ if data.isDead
+ href = Redirect.to 'thread', {boardID, threadID}
+ link = $.el 'a',
+ href: href or "/#{boardID}/res/#{threadID}"
+ textContent: data.excerpt
+ title: data.excerpt
+
+ 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
+ $.add div, [x, $.tn(' '), link]
+ div
+ refresh: ->
+ nodes = []
+ for {boardID, threadID, data} in ThreadWatcher.getAll()
+ nodes.push ThreadWatcher.makeLine boardID, threadID, data
+
+ {list} = ThreadWatcher
+ $.rmAll list
+ $.add list, nodes
+
+ for threadID, thread of g.BOARD.threads
+ toggler = $ '.watcher-toggler', thread.OP.nodes.post
+ toggler.src = if ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
+ Favicon.default
+ else
+ Favicon.empty
+
+ for refresher in ThreadWatcher.menu.refreshers
+ refresher()
+ return
toggle: (thread) ->
- unless $.hasClass $('.watch-thread-link', thread.OP.nodes.post), 'watched'
- ThreadWatcher.watch thread
+ boardID = thread.board.ID
+ threadID = thread.ID
+ if ThreadWatcher.db.get {boardID, threadID}
+ ThreadWatcher.rm boardID, threadID
else
- ThreadWatcher.unwatch thread.board, thread.ID
+ ThreadWatcher.add thread
+ add: (thread) ->
+ data = {}
+ boardID = thread.board.ID
+ threadID = thread.ID
+ if thread.isDead
+ if Conf['Auto Prune'] and ThreadWatcher.db.get {boardID, threadID}
+ ThreadWatcher.rm boardID, threadID
+ return
+ data.isDead = true
+ data.excerpt = Get.threadExcerpt thread
+ ThreadWatcher.db.set {boardID, threadID, val: data}
+ ThreadWatcher.refresh()
+ rm: (boardID, threadID) ->
+ ThreadWatcher.db.delete {boardID, threadID}
+ ThreadWatcher.refresh()
- unwatch: (board, threadID) ->
- $.get 'WatchedThreads', {}, (item) ->
- watched = item['WatchedThreads']
- delete watched[board][threadID]
- delete watched[board] unless Object.keys(watched[board]).length
- ThreadWatcher.refresh watched
- $.set 'WatchedThreads', watched
+ convert: (oldFormat) ->
+ newFormat = {}
+ for boardID, threads of oldFormat
+ for threadID, data of threads
+ (newFormat[boardID] or= {})[threadID] = excerpt: data.textContent
+ newFormat
- watch: (thread) ->
- $.get 'WatchedThreads', {}, (item) ->
- watched = item['WatchedThreads']
- watched[thread.board] or= {}
- watched[thread.board][thread] =
- href: "/#{thread.board}/res/#{thread}"
- textContent: Get.threadExcerpt thread
- ThreadWatcher.refresh watched
- $.set 'WatchedThreads', watched
\ No newline at end of file
+ menu:
+ refreshers: []
+ init: ->
+ return if !Conf['Thread Watcher']
+ menu = new UI.Menu 'thread watcher'
+ $.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) ->
+ menu.toggle e, @, ThreadWatcher
+ @addHeaderMenuEntry()
+ @addMenuEntries()
+
+ addHeaderMenuEntry: ->
+ return if g.VIEW isnt 'thread'
+ entryEl = $.el 'a',
+ href: 'javascript:;'
+ $.event 'AddMenuEntry',
+ type: 'header'
+ el: entryEl
+ order: 60
+ $.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
+ @refreshers.push ->
+ [addClass, rmClass, text] = if $ '.current', ThreadWatcher.list
+ ['unwatch-thread', 'watch-thread', 'Unwatch thread']
+ else
+ ['watch-thread', 'unwatch-thread', 'Watch thread']
+ $.addClass entryEl, addClass
+ $.rmClass entryEl, rmClass
+ entryEl.textContent = text
+
+ addMenuEntries: ->
+ entries = []
+
+ # `Open all` entry
+ entries.push
+ cb: ThreadWatcher.cb.openAll
+ entry:
+ type: 'thread watcher'
+ el: $.el 'a',
+ 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:
+ type: 'thread watcher'
+ 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
+ entries.push
+ cb: ThreadWatcher.cb.pruneDeads
+ entry:
+ type: 'thread watcher'
+ el: $.el 'a',
+ textContent: 'Prune 404\'d threads'
+ refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
+
+ # `Settings` entries:
+ subEntries = []
+ for name, conf of Config.threadWatcher
+ subEntries.push @createSubEntry name, conf[1]
+ entries.push
+ entry:
+ type: 'thread watcher'
+ el: $.el 'span',
+ textContent: 'Settings'
+ subEntries: subEntries
+
+ for {entry, cb, refresh} in entries
+ entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
+ $.on entry.el, 'click', cb if cb
+ @refreshers.push refresh.bind entry if refresh
+ $.event 'AddMenuEntry', entry
+ createSubEntry: (name, desc) ->
+ entry =
+ type: 'thread watcher'
+ el: $.el 'label',
+ innerHTML: " #{name}"
+ title: desc
+ input = entry.el.firstElementChild
+ input.checked = Conf[name]
+ $.on input, 'change', $.cb.checked
+ $.on input, 'change', ThreadWatcher.refresh if name is 'Current Board'
+ entry
diff --git a/src/Posting/QuickReply.coffee b/src/Posting/QuickReply.coffee
index 332ab2fe8..78d4b12be 100644
--- a/src/Posting/QuickReply.coffee
+++ b/src/Posting/QuickReply.coffee
@@ -660,21 +660,9 @@ QR =
cv.width = img.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height
URL.revokeObjectURL fileURL
- applyBlob = (blob) =>
+ cv.toBlob (blob) =>
@URL = URL.createObjectURL blob
@nodes.el.style.backgroundImage = "url(#{@URL})"
- if cv.toBlob
- cv.toBlob applyBlob
- return
- data = atob cv.toDataURL().split(',')[1]
-
- # DataUrl to Binary code from Aeosynth's 4chan X repo
- l = data.length
- ui8a = new Uint8Array l
- for i in [0...l]
- ui8a[i] = data.charCodeAt i
-
- applyBlob new Blob [ui8a], type: 'image/png'
fileURL = URL.createObjectURL @file
img.src = fileURL
diff --git a/src/Quotelinks/QuoteYou.coffee b/src/Quotelinks/QuoteYou.coffee
index b5b9e4a60..b66eb96da 100644
--- a/src/Quotelinks/QuoteYou.coffee
+++ b/src/Quotelinks/QuoteYou.coffee
@@ -16,7 +16,6 @@ QuoteYou =
cb: @node
node: ->
- # Stop there if it's a clone.
return if @isClone
if @info.yours