diff --git a/js/thread-watcher.js b/js/thread-watcher.js
new file mode 100644
index 00000000..22656e48
--- /dev/null
+++ b/js/thread-watcher.js
@@ -0,0 +1,195 @@
+// Thanks to Khorne on #8chan at irc.rizon.net
+// https://gitlab.com/aymous/8chan-watchlist
+
+'use strict';
+/* jshint globalstrict:true, quotmark:single */
+/* jshint browser:true, jquery:true, devel:true, unused:true, undef:true */
+/* global active_page:false, board_name:false */
+if(!localStorage.watchlist){
+ //If the watchlist is undefined in the localStorage,
+ //initialize it as an empty array.
+ localStorage.watchlist = '[]';
+}
+var watchlist = {};
+
+/**
+ * [render /> Creates a watchlist container and populates it with info
+ * about each thread that's currently being watched. If the watchlist container
+ * already exists, it empties it out and repopulates it.]
+ * @param {[Bool]} reset [If true and the watchlist is rendered, remove it]
+ */
+watchlist.render = function(reset) {
+ /* jshint eqnull:true */
+ if (reset == null) reset = false;
+ /* jshint eqnull:false */
+ if (reset && $('#watchlist').length) $('#watchlist').remove();
+ var threads = [];
+ //Read the watchlist and create a new container for each thread.
+ JSON.parse(localStorage.watchlist).forEach(function(e, i) {
+ //look at line 69, that's what (e) is here.
+ threads.push('
' +
+ '
Board: '+e[0]+' ' +
+ '
Thread: '+''+e[1]+' ' +
+ '
Replies: '+e[2]+' ' +
+ '
[Unwatch]'+
+ '
');
+ });
+ if ($('#watchlist').length) {
+ //If the watchlist is already there, empty it and append the threads.
+ $('#watchlist').children('.watchlist-inner').remove();
+ $('#watchlist').append(threads.join(''));
+ } else {
+ //If the watchlist has not yet been rendered, create it.
+ $('form[name="post"]').before(
+ $(''+
+ '
'+
+ threads.join('')+
+ '
').css({
+ background: $('.reply').css('background'),
+ borderColor : $('.reply').css('border-color')
+ }));
+ }
+ return this;
+};
+
+/**
+ * [add /> adds the given item to the watchlist]
+ * @param {[Obj/Str]} sel [An unwrapped jquery selector.]
+ */
+watchlist.add = function(sel) {
+ var threadName, threadInfo;
+
+ if (active_page === 'thread') {
+ if ($('.subject').length){
+ //If a subject is given, use the first 20 characters as the thread name.
+ threadName = $('.subject').text().substring(0,20);
+ } else { //Otherwise use the thread id.
+ threadName = $('.op').parent().attr('id');
+ }
+ //board name, thread name as defined above, current amount of posts, thread url
+ threadInfo = [board_name, threadName, $('.post').length, location.href];
+
+ } else if (active_page === 'index') {
+
+ var postCount;
+ //Figure out the post count.
+ if ($(sel).parents('.op').children('.omitted').length) {
+ postCount = $(sel).parents('.op').children('.omitted').text().split(' ')[0];
+ } else {
+ postCount = $(sel).parents('.op').siblings('.post').length+1;
+ }
+ //Grab the reply link.
+ var threadLink = $(sel).siblings('a:contains("[Reply]")').attr('href');
+ //Figure out the thread name. If anon, use the thread id.
+ if ($(sel).parent().find('.subject').length) {
+ threadName = $(sel).parent().find('.subject').text().substring(0,20);
+ } else {
+ threadName = $(sel).parents('div').last().attr('id');
+ }
+
+ threadInfo = [board_name, threadName, postCount, threadLink];
+
+ } else {
+ alert('Functionality not yet implemented for this type of page.');
+ return this;
+ }
+
+ //if the thread is already being watched, cancel the function.
+ if (localStorage.watchlist.indexOf(JSON.stringify(threadInfo)) !== -1) {
+ return this;
+ }
+
+ var _watchlist = JSON.parse(localStorage.watchlist); //Read the watchlist
+ _watchlist.push(threadInfo); //Add the new watch item.
+ localStorage.watchlist = JSON.stringify(_watchlist); //Save the watchlist.
+ return this;
+};
+
+/**
+ * [remove /> removes the given item from the watchlist]
+ * @param {[Int]} n [The index at which to remove.]
+ */
+watchlist.remove = function(n) {
+ var _watchlist = JSON.parse(localStorage.watchlist);
+ _watchlist.splice(n, 1);
+ localStorage.watchlist = JSON.stringify(_watchlist);
+ return this;
+};
+
+/**
+ * [clear /> resets the watchlist to the initial empty array]
+ */
+watchlist.clear = function() {
+ localStorage.watchlist = '[]';
+ return this;
+};
+
+/**
+ * [exists /> pings every watched thread to check if it exists and removes it if not]
+ * @param {[Obj/Str]} sel [an unwrapped jq selector]
+ */
+watchlist.exists = function(sel) {
+ $.ajax($(sel).children().children('a').attr('href'), {
+ type :'HEAD',
+ error: function() {
+ watchlist.remove(parseInt($(sel).attr('id').split('-')[1])).render();
+ },
+ success : function(){
+ return;
+ }
+ });
+};
+
+$(document).ready(function(){
+ //Append the watchlist toggle button.
+ $('.boardlist').append('[ watchlist ]');
+ //Append a watch thread button after every OP.
+ $('.op>.intro').append('[Watch Thread]');
+
+ //Draw the watchlist, hidden.
+ watchlist.render();
+
+ //Show or hide the watchlist.
+ $('#watchlist-toggle').on('click', function(e) {
+ //if ctrl+click, reset the watchlist.
+ if (e.ctrlKey) {
+ watchlist.render(true);
+ }
+ if ($('#watchlist').css('display') !== 'none') {
+ $('#watchlist').css('display', 'none');
+ } else {
+ $('#watchlist').css('display', 'block');
+ } //Shit got really weird with hide/show. Went with css manip. Probably faster anyway.
+ });
+
+ //Trigger the watchlist add function.
+ //The selector is passed as an argument in case the page is not a thread.
+ $('.watchThread').on('click', function() {
+ watchlist.add(this).render();
+ });
+
+ //The index is saved in .watchlist-inner so that it can be passed as the argument here.
+ //$('.watchlist-remove').on('click') won't work in case of re-renders and
+ //the page will need refreshing. This works around that.
+ $(document).on('click', '.watchlist-remove', function() {
+ var item = parseInt($(this).parent().attr('id').split('-')[1]);
+ watchlist.remove(item).render();
+ });
+
+ //Empty the watchlist and redraw it.
+ $('#clearList').on('click', function(){
+ watchlist.clear().render();
+ });
+
+ //Get rid of every watched item that no longer directs to an existing page.
+ $('#clearGhosts').on('click', function() {
+ $('.watchlist-inner').each(function(){
+ watchlist.exists(this);
+ });
+ });
+
+});
+
diff --git a/stylesheets/style.css b/stylesheets/style.css
index 30172427..85909dd0 100644
--- a/stylesheets/style.css
+++ b/stylesheets/style.css
@@ -913,3 +913,28 @@ span.pln {
clear: none;
}
}
+
+/* threadwatcher */
+
+#watchlist {
+ display: none;
+ max-height: 250px;
+ overflow: auto;
+ border: 1px solid;
+ border-style: none solid solid none;
+ width: 50%;
+ margin: 0 auto;
+ margin-bottom: 10px;
+}
+
+.watchlist-inner, .watchlist-controls {
+ margin: 0 auto;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ text-align: center;
+}
+
+#watchlist-toggle, .watchThread, .watchlist-remove, #clearList, #clearGhosts {
+ cursor: pointer;
+}
+