Reddit highlight newest comments

Highlights new comments in a thread since your last visit

  1. // ==UserScript==
  2. // @name Reddit highlight newest comments
  3. // @description Highlights new comments in a thread since your last visit
  4. // @namespace https://greasyfork.org/users/98-jonnyrobbie
  5. // @author JonnyRobbie
  6. // @include /https?:\/\/((www|pay|[a-z]{2})\.)?reddit\.com\/r\/[a-zA-Z0-9_-]+\/comments\/.*/
  7. // @version 1.5.7
  8. // ==/UserScript==
  9.  
  10. /*-----settings-----*/
  11. highlightBGColorB = 'AECDFF'; //background color of a highlighted newest comment
  12. highlightBGColorA = 'E5EFFF'; //background color of a highlighted older comment
  13. highlightHalf = 2 //[hours]; when the algorithm should interpolate exactly 0.5 between A and B
  14. highlightBGBorder = '1px solid #CDDAF3'; //border style of a highlighted new comment
  15. expiration = 432000000; //expiraton time in millisesonds; default is 5 days (432000000ms)
  16. highlihhtBetterChild = true; //highlight child comment if it has better karma than its parent
  17. highlightNegative = true;
  18. betterChildStyle = '3px solid'; //border of said comment
  19. betterChildStyleGradientA = '99AAEE';
  20. betterChildStyleGradientB = 'F55220';
  21. betterChildStyleGradientC = 'ad3429';
  22. /*-----settings-----*/
  23.  
  24. /*
  25. Changelog:
  26. 1.5.7
  27. -expand subreddit names
  28. 1.5.6
  29. -fix double UI when viewing thread as a mod
  30. 1.5.5
  31. -now works on subdomains
  32. -now works in subreddits with alphanumeric characters
  33. -some minor style changes
  34. 1.5.4
  35. -fixed not working
  36. 1.5.3
  37. -fixed a bog which has caused the script to hkald when the comment was too young to display score
  38. 1.5.2
  39. -some more fugs resulting from reddit changes
  40. 1.5.1
  41. -fixed reddit changes
  42. 1.4.2
  43. -tweaked some colors and timing
  44. 1.4.1
  45. -added highlighting comment with negative karma
  46. -added color shading dependent on the new comment age
  47. -added an option to manually edit the time of the last thread visit
  48. 1.3.1
  49. -Added expiration for localstorage, defaulted to 14 days. It won't grow indefinately now.
  50. -Reduced the amount of console.log clutter
  51. */
  52.  
  53. haveGold = false; //inicialization
  54.  
  55. function isNumber(n) {
  56. return !isNaN(parseFloat(n)) && isFinite(n);
  57. }
  58.  
  59.  
  60. function hasClass(element, cls) {
  61. return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;
  62. }
  63.  
  64. function getThread() {
  65. console.log("Logging reddit comment highlight.");
  66. console.log("Fetching thread ID...");
  67. if (document.querySelector('[rel="shorturl"]') === null) {
  68. console.log("Not a comment thread. Aborting userscript...");
  69. return;
  70. }
  71. haveGold = hasClass(document.getElementsByTagName("body")[0], "gold") || hasClass(document.getElementsByTagName("body")[0], "moderatord");
  72. console.log("hasgold or is a mod: " + haveGold);
  73. purgeOldStorage(expiration);
  74. var threadID = "redd_id_" + document.querySelector('[rel="shorturl"]').href.substr(-6);
  75. console.log("Thread id: " + threadID);
  76. var lastVisit = localStorage.getItem(threadID);
  77. if (lastVisit === null) {
  78. console.log("Thread has not been visited yet. Creating localStorage...");
  79. localStorage.setItem(threadID, Date.parse(Date()));
  80. }
  81. else {
  82. var postMenu;
  83. postMenu = document.getElementById("siteTable").getElementsByClassName("flat-list")[0];
  84. if (!haveGold) {
  85. var timeDiff = Date.parse(Date()) - lastVisit;
  86. var goldBox = document.createElement("div");
  87. goldBox.className = "rounded gold-accent comment-visits-box";
  88. goldBox.style.marginLeft = "10px";
  89. var goldBoxTitle = document.createElement("div");
  90. goldBoxTitle.className = "title";
  91. var goldBoxLabel = document.createElement("span");
  92. goldBoxLabel.innerHTML = "Highlight comments posted since previous visit [hh:mm] ago:";
  93. var goldBoxInput = document.createElement("input");
  94. goldBoxInput.type = "text";
  95. goldBoxInput.style.margin = "auto 5px";
  96. goldBoxInput.style.width = "64px";
  97. goldBoxInput.id = "timeInput"
  98. goldBoxInput.value = padToTwo(Math.floor(timeDiff/(1000*3600))) + ":" + padToTwo(Math.floor(60*(timeDiff/(1000*3600) - Math.floor(timeDiff/(1000*3600)))));
  99. var goldBoxOK = document.createElement("input");
  100. goldBoxOK.type = "button";
  101. goldBoxOK.value = "OK";
  102. goldBoxOK.addEventListener("click", function(){timeSetBack(threadID, goldBoxInput);}, false)
  103. goldBox.appendChild(goldBoxTitle);
  104. goldBoxTitle.appendChild(goldBoxLabel);
  105. goldBoxTitle.appendChild(goldBoxInput);
  106. goldBoxTitle.appendChild(goldBoxOK);
  107. insertNodeAfter(goldBox, document.getElementsByClassName("commentarea")[0].getElementsByClassName("usertext")[0]);
  108. }
  109. }
  110. commentReg = /^https?:\/\/(www\.)reddit\.com\/r\/[a-zA-Z0-9_-]+\/comments\/[0-9a-z]+\/[0-9a-z_]+\/$/;
  111. isCommentPage = commentReg.test(document.URL);
  112. highlightComments(lastVisit, isCommentPage);
  113. console.log("Match for full comment page(" + document.URL + "): " + isCommentPage);
  114. if (isCommentPage == true) {
  115. console.log("Setting localStorage to now...");
  116. localStorage.setItem(threadID, Date.parse(Date()));
  117. } else
  118. {
  119. console.log("not a comment page")
  120. }
  121. permalinkReg = /^(https?:\/\/(www\.)reddit\.com\/r\/[a-zA-Z0-9_-]+\/comments\/[0-9a-z]+\/[0-9a-z_]+\/[0-9a-z]+)((\?context=([0-9])+)|\/)?$/;
  122. ispermalinkPage = permalinkReg.test(document.URL);
  123. if (ispermalinkPage == true) {
  124. var context = permalinkReg.exec(document.URL)[5];
  125. var nocontext = permalinkReg.exec(document.URL)[1];
  126. console.log("perma RegEx: " + nocontext)
  127. if (typeof context == 'undefined') {
  128. console.log("context not set");
  129. context = 0;
  130. }
  131. if (context > 5) {
  132. console.log("context greater than 5");
  133. context = 5;
  134. }
  135. console.log("permalink page...context " + context);
  136. addContext(context, permalinkReg.exec(document.URL)[1]);
  137. } else
  138. {
  139. console.log("not a permalink page");
  140. }
  141. }
  142.  
  143. function addContext(context, nocontext) {
  144. var newSelect = document.createElement("select");
  145. var infobar = document.getElementsByClassName("commentarea")[0].getElementsByClassName("infobar")[0];
  146. newSelect.addEventListener("change", function(){window.location=nocontext + '?context=' + this.selectedIndex;}, false);
  147. var newOption;
  148. for (var c=0; c<=5; c++) {
  149. var newOption = document.createElement("option");
  150. if (c == context) {
  151. newOption.selected = "selected";
  152. }
  153. newOption.value = nocontext + "?context=" + c;
  154. newOption.innerHTML = "Context: " + c;
  155. newSelect.appendChild(newOption);
  156. }
  157. infobar.appendChild(newSelect);
  158. }
  159.  
  160. function padToTwo(number) {
  161. if (number<=99) { number = ("0"+number).slice(-2); }
  162. return number;
  163. }
  164.  
  165.  
  166. function timeSetBack(threadID, textbox){
  167. console.log("setting time back");
  168. var timeBackArray = textbox.value.split(":")
  169. if (timeBackArray.length > 2) {
  170. alert("You have not entered a valid time");
  171. console.log("too many colons");
  172. return
  173. }
  174. var timeBack = parseInt(timeBackArray[0], 10) + (parseInt(timeBackArray[1], 10)/60)
  175. if (timeBack != null && isNumber(timeBack) == true) {
  176. var lastVisit = Date.parse(Date())-timeBack*3600000;
  177. if (lastVisit > localStorage.getItem(threadID)) alert("The time has been set lower than it was before. Please, refresh the page to properly display the changes.");
  178. localStorage.setItem(threadID, lastVisit);
  179. console.log("Setting localStorage " + threadID + " " + timeBack*3600000 + " ms back");
  180. highlightComments(lastVisit, true);
  181. }
  182. }
  183.  
  184. function purgeOldStorage(difference) {
  185. var thisTime = Date.parse(Date());
  186. var total=0;
  187. for (var i=0;i<localStorage.length;i++) {
  188. if (localStorage.key(i).substr(0,8)=="redd_id_" && parseInt(localStorage.getItem(localStorage.key(i)))+difference<thisTime) {
  189. localStorage.removeItem(localStorage.key(i));
  190. total++;
  191. i=0;
  192. }
  193. }
  194. console.log(total + " localStorage older than " + difference + "ms has been removed");
  195. }
  196.  
  197. function isProperThread() {
  198. return true;
  199. }
  200.  
  201. function insertNodeAfter(node, sibling) {
  202. sibling.parentNode.insertBefore(node, sibling.nextSibling);
  203. }
  204.  
  205. function highlightComments(lastVisit, isCommentPage) {
  206. console.log("Thread last visited in: " + lastVisit);
  207. comments = document.getElementsByClassName('comment');
  208. console.log("Comment count: " + comments.length);
  209. nowtime = Date.parse(Date());
  210. highlightHalf = Math.pow(0.5, (1/(-highlightHalf)));
  211. for(i=0; i<comments.length; i++) {
  212. if ((' ' + comments[i].className + ' ').indexOf(' deleted ') > -1) {continue;} //if the comment contains class 'deleted', skip this iteration
  213. var ctime = Date.parse(comments[i].childNodes[2].childNodes[0].getElementsByTagName('time')[0].getAttribute('title'));
  214. var scoreTag = comments[i].getElementsByClassName("tagline")[0].getElementsByClassName("unvoted");
  215. if (scoreTag.length > 0) {
  216. scorechild = parseInt(scoreTag[0].innerHTML);
  217. } else {
  218. scorechild = 0;
  219. }
  220. var scoreTag = comments[i].parentNode.parentNode.parentNode.getElementsByClassName("tagline")[0].getElementsByClassName("unvoted");
  221. if (scoreTag.length > 0) {
  222. scoreparent = parseInt(scoreTag[0].innerHTML);
  223. } else {
  224. scoreparent = 0;
  225. }
  226. var voted = comments[i].getElementsByClassName("midcol")[0].className;
  227. if (voted == "midcol likes") {
  228. scorechild++;
  229. } else if (voted == "midcol dislikes") {
  230. scorechild--;
  231. }
  232. var voted = comments[i].parentNode.parentNode.parentNode.getElementsByClassName("midcol")[0].className
  233. if (voted == "midcol likes") {
  234. scoreparent++;
  235. } else if (voted == "midcol dislikes") {
  236. scoreparent--;
  237. }
  238. if (parseInt(ctime) > parseInt(lastVisit) && haveGold == false) {
  239. usertextBody = comments[i].getElementsByClassName("usertext-body")[0];
  240. console.log("comment(" + i + "," + 0 + "): gradient(" + nowtime + "," + ctime + ") = " + getGradient(nowtime-ctime));
  241. usertextBody.style.backgroundColor = interpolateColor(highlightBGColorA, highlightBGColorB, getGradient(nowtime-ctime));
  242. usertextBody.style.sborder = highlightBGBorder;
  243. }
  244. if (scoreparent < scorechild && highlihhtBetterChild == true && comments[i].parentNode.parentNode.parentNode.className != "content") {
  245. comments[i].style.setProperty('border-left',betterChildStyle + " #" + betterChildStyleGradientA, 'important');
  246. }
  247. if (scorechild < 0 && highlightNegative == true) {
  248. comments[i].style.setProperty('border-left','1px solid #' + betterChildStyleGradientC, 'important');
  249. }
  250. }
  251. if (haveGold == true && isCommentPage == true) {highlightNew();}
  252. var goldSelect = document.getElementById("comment-visits");
  253. if (goldSelect != null) goldSelect.addEventListener("change", function(){console.log("changed highlighting");highlightNew();}, false);
  254. window.addEventListener("load", function(){console.log("window loaded, highlighting");highlightNew();}, false);
  255. }
  256.  
  257. function highlightNew(){
  258. comments = document.getElementsByClassName("new-comment");
  259. console.log("gold " + comments.length + " new comments");
  260. nowtime = Date.parse(Date());
  261. highlightHalf = Math.pow(0.5, (1/(-highlightHalf)));
  262. for(i=0; i<comments.length; i++) {
  263. var titleDate;
  264. titleDate = comments[i].childNodes[2].childNodes[0].getElementsByTagName('time')[0].getAttribute('datetime')
  265. var ctime = Date.parse(titleDate);
  266. usertextBody = comments[i].getElementsByClassName("usertext-body")[0];
  267. console.log("comment(" + i + "," + 0 + "): gradient(" + nowtime + "," + ctime + ") = " + getGradient(nowtime-ctime));
  268. usertextBody.style.backgroundColor = interpolateColor(highlightBGColorA, highlightBGColorB, getGradient(nowtime-ctime));
  269. usertextBody.style.sborder = highlightBGBorder;
  270. }
  271. }
  272.  
  273. function finalBGColor(ctime) {
  274. nowtime = Date.parse(Date());
  275. }
  276.  
  277. function getGradient(time) {
  278. return Math.pow(highlightHalf, -(time/3600000));
  279. }
  280.  
  281. function interpolateColor(minColor,maxColor,depth){
  282. function d2h(d) {return d.toString(16);}
  283. function h2d(h) {return parseInt(h,16);}
  284. if(depth == 0){
  285. return minColor;
  286. }
  287. if(depth == 1){
  288. return maxColor;
  289. }
  290. var color = "#";
  291. for(var i=0; i < 6; i+=2){
  292. var minVal = new Number(h2d(minColor.substr(i,2)));
  293. var maxVal = new Number(h2d(maxColor.substr(i,2)));
  294. var nVal = minVal + (maxVal-minVal) * depth;
  295. var val = d2h(Math.floor(nVal));
  296. while(val.length < 2){
  297. val = "0"+val;
  298. }
  299. color += val;
  300. }
  301. return color;
  302. }
  303.  
  304. function formatDate(unixtime) {
  305. var t = new Date(unixtime);
  306. //return t.toString();
  307. return t.getDate() + "." + (t.getMonth()+1) + "." + t.getFullYear() + " " + (t.getUTCHours()+UTCDifference) + ":" + pad2(t.getMinutes()) + ":" + pad2(t.getSeconds()) + " " + timeZoneDesc;
  308. }
  309. function dateDifference(unixtime) {
  310. var diff = new Date();
  311. var unixtime2 = new Date(unixtime);
  312. var secDiff = (diff.getSeconds() - unixtime2.getSeconds());
  313. var minDiff = (diff.getMinutes() - unixtime2.getMinutes());
  314. var hourDiff = (diff.getHours() - unixtime2.getHours());
  315. var dayDiff = (diff.getDate() - unixtime2.getDate());
  316. var monthDiff = (diff.getMonth() - unixtime2.getMonth());
  317. var yearDiff = (diff.getFullYear() - unixtime2.getFullYear());
  318. var months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  319. if (secDiff < 0) {
  320. minDiff--;
  321. secDiff += 60;
  322. }
  323. if (minDiff < 0) {
  324. hourDiff--;
  325. minDiff += 60;
  326. }
  327. if (hourDiff < 0) {
  328. dayDiff--;
  329. hourDiff += 24;
  330. }
  331. if (dayDiff < 0) {
  332. monthDiff--;
  333. dayDiff += months[diff.getMonth()-2]; // -2 as the months array is zero-indexed too
  334. }
  335. if (monthDiff < 0) {
  336. yearDiff--;
  337. monthDiff += 12;
  338. }
  339. //console.log(yearDiff+' years, '+monthDiff+' months, '+dayDiff+' days');
  340. return (yearDiff == 0 ? "" : yearDiff + " years ") + (monthDiff == 0 ? "" : monthDiff + " months ") + (dayDiff == 0 ? "" : dayDiff + " days ") + (hourDiff == 0 ? "" : hourDiff + " hours ") + (minDiff == 0 ? "" : minDiff + " minutes ") + (secDiff == 0 ? "" : secDiff + " seconds");
  341. //return yearDiff + " years " + monthDiff + " months " + dayDiff + " days " + hourDiff + " hours " + minDiff + " minutes " + secDiff + " seconds";
  342. }
  343. function addCss(cssCode) {
  344. //thanks for the function from this blog http://www.tomhoppe.com/index.php/2008/03/dynamically-adding-css-through-javascript/
  345. var styleElement = document.createElement("style");
  346. styleElement.type = "text/css";
  347. if (styleElement.styleSheet) {
  348. styleElement.styleSheet.cssText = cssCode;
  349. } else {
  350. styleElement.appendChild(document.createTextNode(cssCode));
  351. }
  352. document.getElementsByTagName("head")[0].appendChild(styleElement);
  353. }
  354. function pad2(number) {
  355. return (number < 10 ? '0' : '') + number
  356. }
  357. getThread();