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.

当前为 2016-12-28 提交的版本,查看 最新版本

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