Thread Watcher

Watch threads for new posts.

  1. // ==UserScript==
  2. // @name Thread Watcher
  3. // @namespace com.kongregate.resterman
  4. // @author resterman
  5. // @version 1.0.1
  6. // @include http://www.kongregate.com/community*
  7. // @include http://www.kongregate.com/forums/*
  8. // @description Watch threads for new posts.
  9. // ==/UserScript==
  10.  
  11.  
  12.  
  13. function Thread(threadId, threadTitle, lastPostId, lastPostAuthor, forumId) {
  14. this.threadId = threadId;
  15. this.threadTitle = threadTitle;
  16. this.lastPostId = lastPostId;
  17. this.lastPostAuthor = lastPostAuthor;
  18. this.forumId = forumId;
  19. }
  20.  
  21. Thread.prototype = {
  22.  
  23. THREADS_KEY: "thread",
  24.  
  25. save: function () {
  26. var threadsWatched = localStorage.getItem(this.THREADS_KEY);
  27. if (threadsWatched === null)
  28. threadsWatched = {};
  29. else
  30. threadsWatched = JSON.parse(threadsWatched);
  31.  
  32. threadsWatched[this.threadId] = {
  33. threadId: this.threadId,
  34. forumId: this.forumId,
  35. lastPostId: this.lastPostId,
  36. threadTitle: this.threadTitle,
  37. lastPostAuthor: this.lastPostAuthor
  38. };
  39.  
  40. localStorage.setItem(this.THREADS_KEY, JSON.stringify(threadsWatched));
  41. },
  42.  
  43. watch: function () {
  44. this.save();
  45. },
  46.  
  47. unwatch: function () {
  48. var threadsWatched = JSON.parse(localStorage.getItem(this.THREADS_KEY));
  49. if (threadsWatched === null || !this.isWatched())
  50. return;
  51.  
  52. delete threadsWatched[this.threadId];
  53. localStorage.setItem(this.THREADS_KEY, JSON.stringify(threadsWatched));
  54. },
  55.  
  56. wasUpdated: function () {
  57. if (!this.isWatched())
  58. return false;
  59.  
  60. var storedThread = Thread.get(this.threadId);
  61. return storedThread ? storedThread.lastPostId < this.lastPostId : false;
  62. },
  63.  
  64. isOlder: function (aThread) {
  65. return this.lastPostId < aThread.lastPostId;
  66. },
  67.  
  68. isWatched: function () {
  69. return !!Thread.get(this.threadId);
  70. },
  71.  
  72. getUrl: function () {
  73. return '/forums/'+ this.forumId +'/topics/'+ this.threadId;
  74. },
  75.  
  76. createWatchButton: function () {
  77. var link = new Element('a', {href: 'javascript:void(0);'})
  78. .update(this.isWatched() ? Thread.UNWATCH : Thread.WATCH);
  79. link.setAttribute('class', this.isWatched() ? 'unwatch-btn' : 'watch-btn');
  80.  
  81. var self = this;
  82. link.observe('click', function (e) {
  83. e.stop();
  84.  
  85. if (self.isWatched()) {
  86. self.unwatch();
  87. link.update(Thread.WATCH)
  88. .setAttribute('class', 'watch-btn');
  89. } else {
  90. self.watch();
  91. link.update(Thread.UNWATCH)
  92. .setAttribute('class', 'unwatch-btn');
  93. }
  94. });
  95.  
  96. return link;
  97. }
  98.  
  99. };
  100.  
  101. Thread.WATCH = 'watch';
  102. Thread.UNWATCH = 'unwatch';
  103.  
  104. Thread.getPostIdFromUrl = function (url) {
  105. var matches = url.match(/posts-([0-9]+)-row/);
  106. return matches !== null ? parseInt(matches[1]) : null;
  107. };
  108.  
  109. Thread.getThreadIdFromUrl = function (url) {
  110. var matches = url.match(/topics\/([0-9]+)-/);
  111. return matches !== null ? parseInt(matches[1]) : null;
  112. };
  113.  
  114. Thread.getForumIdFromUrl = function (url) {
  115. var matches = url.match(/forums\/([0-9]+)-/);
  116. return matches !== null ? parseInt(matches[1]) : null;
  117. };
  118.  
  119. Thread.create = function (threadId, threadTitle, lastPostId, lastPostAuthor, forumId) {
  120. return new Thread(threadId, threadTitle, lastPostId, lastPostAuthor, forumId);
  121. };
  122.  
  123. Thread.createFromUrl = function (url) {
  124. return Thread.create(
  125. Thread.getThreadIdFromUrl(url),
  126. null,
  127. Thread.getPostIdFromUrl(url),
  128. null,
  129. Thread.getForumIdFromUrl(url)
  130. );
  131. };
  132.  
  133. Thread.get = function (threadId) {
  134. var threadsWatched = Thread.getAllWatched();
  135. if (threadsWatched === null)
  136. return null;
  137.  
  138. return threadsWatched[threadId];
  139. };
  140.  
  141. Thread.getAllWatched = function () {
  142. var threadsWatched = localStorage.getItem(Thread.prototype.THREADS_KEY);
  143. if (threadsWatched === null)
  144. return null;
  145.  
  146. threadsWatched = JSON.parse(threadsWatched);
  147. for (var i in threadsWatched) {
  148. var obj = threadsWatched[i];
  149. threadsWatched[i] = Thread.create(
  150. obj.threadId,
  151. obj.threadTitle,
  152. obj.lastPostId,
  153. obj.lastPostAuthor,
  154. obj.forumId
  155. );
  156. }
  157.  
  158. return threadsWatched;
  159. };
  160.  
  161. /* url: http://www.kongregate.com/forums/ */
  162. function threads(){
  163. var css = document.createElement('style');
  164. css.innerHTML = 'td.lp span a.unwatch-btn { color: #336699; } td.lp span a.unwatch-btn { color: #900; }';
  165. document.head.appendChild(css);
  166.  
  167. var threads = $$('.hentry');
  168. threads.each(function (thread) {
  169. var links = thread.select('a');
  170. var url = links[links.length - 1].href;
  171. var t = Thread.createFromUrl(url);
  172. t.threadTitle = thread.select('.entry-title')[0].innerText;
  173. t.lastPostAuthor = thread.select('.author')[0].firstChild.innerText;
  174.  
  175. var actionLink = t.createWatchButton();
  176. actionLink.setStyle({
  177. 'margin-left': '2px'
  178. });
  179. thread.select('.lp')[0]
  180. .insert(new Element('span').insert(actionLink));
  181.  
  182. if (t.isWatched() && t.wasUpdated()) {
  183. thread.select('.icon')[0].setStyle({
  184. transition: 'all ease 0.5s',
  185. backgroundColor: 'deepskyblue'
  186. });
  187. }
  188. });
  189. }
  190.  
  191. // url: http://www.kongregate.com/forums/*/topics/*
  192. function thread() {
  193. var id = Thread.getThreadIdFromUrl(location.href),
  194. thread = Thread.get(id);
  195. var titleClone = $$('.forum_header h1')[0].clone(true);
  196. var threadTools = titleClone.select('#topic_mod')[0];
  197. if (threadTools)
  198. threadTools.remove();
  199. var threadTitle = titleClone.innerText.match(/(.*?)(\s+page\s+[0-9]+|$)/m)[1];
  200. if (!thread) {
  201. thread = Thread.createFromUrl(location.href);
  202. // Avoid fetching real last id, setting to negative id
  203. thread.lastPostId = -1;
  204. thread.lastPostAuthor = null; // Doesn't matter
  205.  
  206. thread.threadTitle = threadTitle;
  207. }
  208. if (thread.isWatched() && thread.threadTitle !== threadTitle) {
  209. thread.threadTitle = threadTitle;
  210. thread.save();
  211. }
  212.  
  213. var lastPost = $$('.post:last')[0];
  214. if (!lastPost)
  215. return;
  216.  
  217. var lastId = lastPost.getAttribute('id').match(/posts-([0-9]+)-row/)[1];
  218. if (thread.isWatched() && lastId > thread.lastPostId) {
  219. thread.lastPostId = lastId;
  220. thread.save();
  221. }
  222.  
  223. var watchButton = thread.createWatchButton().setStyle({ marginLeft: '10px' });
  224. $$('.media.mbs').each(function (i) {
  225. i.select('.utility').each(function (j) {
  226. j.insert({
  227. after: watchButton
  228. });
  229. });
  230. });
  231. }
  232.  
  233.  
  234. /* url: http://www.kongregate.com/community/ */
  235. function community() {
  236. var containerTitle = new Element('h3', {
  237. id: 'watched_threads_title',
  238. class: 'forum_group_title h2 mtl'
  239. }).update('Watched Threads');
  240.  
  241. var threadsTable = new Element('table');
  242. $('forums_title').parentNode.insert({ bottom: containerTitle });
  243. $('forums_title').parentNode.insert({ bottom: threadsTable });
  244.  
  245. var threadsTableBody = new Element('tbody');
  246. threadsTable.insert(threadsTableBody);
  247.  
  248. var onUnwatchClick = function (thread, row) {
  249. return function(e) {
  250. e.stop();
  251. thread.unwatch();
  252. row.remove();
  253. };
  254. };
  255.  
  256. var threads = Thread.getAllWatched();
  257. for (var i in threads) {
  258. var t = threads[i];
  259. var row = new Element('tr');
  260. threadsTableBody.insert(row);
  261.  
  262. var titleContainer = new Element('td', {
  263. class: 'c2 pts'
  264. });
  265. row.insert(titleContainer);
  266.  
  267. var title = new Element('a', {
  268. class: 'title h3',
  269. href: t.getUrl()
  270. }).update(t.threadTitle);
  271. titleContainer.insert(title);
  272.  
  273. var unwatchButton = new Element('a', {
  274. href: 'javascript:void(0);'
  275. }).update('unwatch')
  276. .setStyle({
  277. 'float': 'right'
  278. });
  279.  
  280. unwatchButton.observe('click', onUnwatchClick(t, row));
  281.  
  282. titleContainer.insert(unwatchButton);
  283. }
  284.  
  285. }
  286.  
  287. (function() {
  288. 'use strict';
  289. if (/\.com\/forums\/.*\/topics/.test(location.href))
  290. thread();
  291. else if (/\.com\/forums/.test(location.href))
  292. threads();
  293. else if (/\.com\/community/.test(location.href))
  294. community();
  295. })();