Reddit - Unread Comment Helper (fork)

On topic pages, show "X unread comments (Y total)"; on comment pages, highlight unread comments. Local storage only -- does not work across multiple computers.

当前为 2015-04-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Reddit - Unread Comment Helper (fork)
  3. // @description On topic pages, show "X unread comments (Y total)"; on comment pages, highlight unread comments. Local storage only -- does not work across multiple computers.
  4. // @author Sam Angove <sam a-inna-circle rephrase period network>
  5. // @namespace http://rephrase.net/box/user-js/
  6. // @include http://reddit.com/
  7. // @include http://reddit.com/?*
  8. // @include http://reddit.com/r/*
  9. // @include http://www.reddit.com/
  10. // @include http://www.reddit.com/*
  11. // @include http://www.reddit.com/r/*
  12. // @version f.5.3
  13. // ==/UserScript==
  14.  
  15. /*
  16. Features:
  17.  
  18. * Replaces "10 comments" with "5 unread comments (10 total)"
  19. * The unread comments link goes directly to the first unread comment
  20. * Unread comments are highlighted for ease of skimming
  21. * You can navigate through unread comments with the following hotkeys:
  22. Alt+Q / Alt+W -or- Ctrl+Up arrow / Ctrl+Down arrow
  23.  
  24. Version history:
  25.  
  26. 0.1.0 - 2007-06-18 - initial release
  27. 0.2.0 - 2008-05-27 - revise to work with new Reddit, and finally remove
  28. the (long superfluous) cross-subdomain messaging crap
  29. 0.2.1 - 2008-07-23 - update to work with new URL form
  30. 0.2.2 - 2009-02-03 - update to work with new HTML
  31. 0.2.3 - 2009-06-03 - update to handle additional classnames with "entry"
  32. 0.2.4 - 2009-06-03 - fix "jump to new comment"
  33. 0.3.0 - 2010-02-19 - rewrite to use localStorage instead of Google Gears;
  34. exclude comscore iframe
  35. 0.3.1 - 2010-02-24 - don't @exclude valid links with 'help' etc. in title,
  36. version stored data, use non-b36 separator
  37. f.4.0 - 2013-10-03 - fix bugs with incorrect unread count, allow to
  38. navigate through unread comments
  39. f.5.0 - 2013-10-27 - delete items older than 7 days to avoid the
  40. localStorage size quota
  41. f.5.1 - 2013-11-06 - fixed a bug in deletion date calculation
  42. f.5.2 - 2014-03-21 - fixed a breakage due to an update of Reddit
  43. */
  44. (function(){
  45.  
  46. var DATA_VERSION = 'ruch2';
  47. var DELETE_OLDER_THAN = 1000*60*60*24*7; // Items older than 7 days
  48. /* Shove an item into local storage */
  49. function getData(id) {
  50. var data = localStorage.getItem(DATA_VERSION + '|' + id);
  51. //console.log("getData", document.location.href, id, data);
  52. if (data === null || data.substr(0, 1) != "{")
  53. return null;
  54. return JSON.parse(data);
  55. }
  56. /* Get an item out of local storage */
  57. function setData(id, data) {
  58. //console.log("setData", document.location.href, id, data);
  59. localStorage.setItem(DATA_VERSION + '|' + id, JSON.stringify(data));
  60. }
  61. /* Delete old items out of local storage */
  62. function deleteOldItems() {
  63. var data = localStorage.getItem(DATA_VERSION + '_last_clean_time');
  64. if (data !== null && Date.now() - data < 1000*60*60*24) { // Cleanup every 24 hours
  65. return;
  66. }
  67. var key, row;
  68. for (var i = 0; i < localStorage.length; i++) {
  69. //console.log(localStorage.key(i));
  70. key = localStorage.key(i);
  71. if (key.lastIndexOf(DATA_VERSION + '|', 0) !== 0) {
  72. continue;
  73. }
  74. data = localStorage.getItem(key);
  75. if (data === null || data.substr(0, 1) != "{") {
  76. continue;
  77. }
  78. row = JSON.parse(data);
  79. if (Date.now() - row["seentime"] < DELETE_OLDER_THAN) {
  80. continue;
  81. }
  82. localStorage.removeItem(key);
  83. }
  84. localStorage.setItem(DATA_VERSION + '_last_clean_time', Date.now());
  85. }
  86. /* Apply a style to highlight unread comments */
  87. function highlightUnread(comment) {
  88. comment.style.setProperty("border-left", "2px solid orange", null);
  89. comment.style.setProperty("padding-left", "5px", null);
  90. comment.classList.add('unreadHighlighted');
  91. }
  92. /* Get sorted array of unread comments offsets. */
  93. function getCommentsOffsetTop() {
  94. var comments = document.getElementsByClassName('unreadHighlighted');
  95. var arr = new Array(comments.length);
  96. for (var i = 0; i < comments.length; i++) {
  97. arr[i] = comments[i].offsetTop;
  98. }
  99. arr.sort(function(a, b) {
  100. return a - b;
  101. });
  102. return arr;
  103. }
  104. /* Jump to the next new comment. */
  105. function jumpToNextComment() {
  106. var unread = getCommentsOffsetTop();
  107. var scrollUnread;
  108. for (var i = 0; i < unread.length; i++) {
  109. scrollUnread = unread[i] - Math.round(window.innerHeight/2);
  110. if (scrollUnread > document.documentElement.scrollTop) {
  111. window.scrollTo(0, scrollUnread);
  112. break;
  113. }
  114. }
  115. }
  116. /* Jump to the previous new comment. */
  117. function jumpToPrevComment() {
  118. var unread = getCommentsOffsetTop().reverse();
  119. var scrollUnread;
  120. for (var i = 0; i < unread.length; i++) {
  121. scrollUnread = unread[i] - Math.round(window.innerHeight/2);
  122. if (scrollUnread < document.documentElement.scrollTop) {
  123. window.scrollTo(0, scrollUnread);
  124. break;
  125. }
  126. }
  127. }
  128.  
  129. /*
  130. Handle a list page, adding "n unread comments" links etc.
  131. */
  132. function handleListPage() {
  133. var snap = document.evaluate("//a" +
  134. "[contains(concat(' ', normalize-space(@class), ' '), ' comments ')]" +
  135. "[contains(@href, '/comments')]",
  136. document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  137.  
  138. var b36tid, row, match, comments, seen, newcomments, newlink;
  139. for(var elm = null, i = 0; (elm = snap.snapshotItem(i)); i++) {
  140. match = elm.firstChild.nodeValue.match(/(\d+) comment/);
  141. // No comments; bail early.
  142. if (!match)
  143. continue;
  144.  
  145. comments = match[1];
  146.  
  147. // Alphanumeric base-36 id, like "1lp5".
  148. b36tid = elm.getAttribute("href").match(/\/comments\/([^\/]+)/)[1];
  149. row = getData(b36tid);
  150.  
  151. seen = row ? row["seencount"] : 0;
  152.  
  153. newcomments = comments - seen;
  154. // Can be negative if comments are deleted.
  155. if (newcomments < 0) newcomments = 0;
  156.  
  157. newlink = elm.cloneNode(false);
  158. if (newcomments > 0)
  159. newlink.style.color = "#333";
  160.  
  161. var cstring = "unread comment" + (newcomments != 1 ? "s" : "");
  162.  
  163. newlink.appendChild( document.createTextNode(newcomments + " " + cstring) );
  164. elm.parentNode.insertBefore(newlink, elm);
  165.  
  166. // Some comments have been read already.
  167. // Show both read and unread counts.
  168. if (comments > newcomments) {
  169. // Magic fragment causes a jump to the first unread comment.
  170. newlink.href += "#new";
  171. elm.removeChild(elm.firstChild);
  172. elm.appendChild(document.createTextNode("(" + comments + " total)"));
  173. elm.style.setProperty("background-color", "#fff", null);
  174. } else {
  175. // All comments are unread.
  176. elm.parentNode.removeChild(elm);
  177. }
  178. }
  179. }
  180.  
  181. /*
  182. Handle a comments page: highlight new comments, save the ID of the highest
  183. comment, etc.
  184. */
  185. function handleCommentsPage() {
  186. var url = document.location.href.split("#");
  187. var frag = url.length > 1 ? url[1] : false;
  188. var b36tid = url[0].match(/\/comments\/([^\/]+)/)[1];
  189.  
  190. var row = getData(b36tid);
  191.  
  192. var update, max_cid = 0, newmax = 0, seencount = 0;
  193. if (row) {
  194. newmax = max_cid = row["max_cid"];
  195. seencount = row["seencount"];
  196. }
  197.  
  198. var comments, i, split, b36cid, cid;
  199. comments = document.getElementsByClassName('comment');
  200. for(i=0; i<comments.length; i++) {
  201. split = comments[i].className.split("_");
  202. if(split.length == 2)
  203. {
  204. b36cid = split[1].split(' ')[0].substr(1);
  205. cid = parseInt(b36cid, 36);
  206. if (cid > max_cid) {
  207. highlightUnread(comments[i].getElementsByClassName("entry")[0]);
  208. if (cid > newmax) {
  209. newmax = cid;
  210. }
  211. }
  212. }
  213. }
  214.  
  215. if (frag == "new") {
  216. jumpToNextComment();
  217. }
  218.  
  219. var elm = document.getElementsByClassName('comments')[0];
  220.  
  221. comments = elm.innerHTML.match(/\b\d+\b/);
  222. if (comments) {
  223. comments = comments[0];
  224. if (comments > 0) {
  225. setData(b36tid, {"max_cid": newmax, "seencount": comments, "seentime": Date.now()});
  226. }
  227. }
  228. }
  229.  
  230. if (document.location.href.match(/\/comments(\/|\?|#|$)/)) {
  231. if (document.location.href.match(/\/comments\/[^\/?#]+(\/([^\/?#]+\/?)?)?(\?|#|$)/)) {
  232. deleteOldItems();
  233. handleCommentsPage();
  234.  
  235. document.addEventListener('keydown', function(e) {
  236. // Alt+Q
  237. if (e.keyCode == "Q".charCodeAt(0) && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
  238. jumpToPrevComment();
  239. e.preventDefault();
  240. return false;
  241. }
  242. // Alt+W
  243. if (e.keyCode == "W".charCodeAt(0) && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
  244. jumpToNextComment();
  245. e.preventDefault();
  246. return false;
  247. }
  248.  
  249. // Ctrl+up
  250. if (e.keyCode == 38 && !e.shiftKey && e.ctrlKey && !e.altKey && !e.metaKey) {
  251. jumpToPrevComment();
  252. e.preventDefault();
  253. return false;
  254. }
  255. // Ctrl+down
  256. if (e.keyCode == 40 && !e.shiftKey && e.ctrlKey && !e.altKey && !e.metaKey) {
  257. jumpToNextComment();
  258. e.preventDefault();
  259. return false;
  260. }
  261. }, false);
  262. }
  263. } else {
  264. handleListPage();
  265. }
  266.  
  267. })();