NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Reddit highlight new comments
// @namespace https://github.com/Farow/userscripts
// @description Highlights new comments since your last visit
// @include /https?:\/\/[a-z]+\.reddit\.com\/r\/[\w:+-]+\/comments\/[\da-z]/
// @version 2.0.2
// @require https://raw.githubusercontent.com/bgrins/TinyColor/master/tinycolor.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @homepageURL https://github.com/Farow/userscripts
// @license MIT
// ==/UserScript==
'use strict';
/*
changelog:
2020-03-14 - 2.0.2 - fixed exception on comments with no live timestamps
2019-02-12 - 2.0.1 - fixed issue with media threads
2016-02-16 - 2.0.0
- removed better/worse comments
- removed option to use reddit's new comment highlighting, it is now removed
- added support for Chromium
- all visits within the past 7 days are show in a dropdown (similar to reddit's highlighting),
you can still choose a custom time
- added UI for settings with a preview, settings won't be reset on every update
- you can now select which part of the comment is highlighted
available options: whole comment, text and time
2015-02-20 - 1.0.2 - added option to use either this script's or reddit's comment highlighting
2014-09-10 - 1.0.1 - no longer highlights your own comments
2014-08-31 - 1.0.0 - initial release
*/
let HNC = {
init: function () {
if (!document.getElementById('siteTable')) {
return;
}
let thread = document.getElementsByClassName('thing link')[0].className.match(/id-(t3_[^ ]+)/)[1],
now = Date.now()
;
this.config = this.cfg.load();
this.clear_history();
if (!this.config.history[thread]) {
this.config.history[thread] = [ ];
}
this.config.history[thread].unshift(now);
/* check for comments */
if (!document.getElementById('noresults')) {
/* highlight */
if (this.config.history[thread].length > 1) {
this.highlight(this.config.history[thread][1]);
}
/* add UI */
this.ui.create_comment_highlighter(this.config.history[thread]);
this.ui.create_config_dialog();
GM_addStyle(this.data.get('config_style'));
}
this.cfg.save();
},
highlight: function (since) {
let comments = document.getElementsByClassName('comment'),
username
;
if (document.body.classList.contains('loggedin')) {
username = document.getElementsByClassName('user')[0].firstElementChild.textContent;
}
for (let comment of comments) {
/* skip removed or deleted comments */
if (comment.classList.contains('deleted') || comment.classList.contains('spam')) {
continue;
}
/* skip our own comments */
let author = comment.getElementsByClassName('author')[0].textContent;
if (username && username == author) {
continue;
}
/* select original or edited comment time */
let times = comment.getElementsByClassName('tagline')[0].getElementsByTagName('time'),
time = Date.parse(times[this.config.prefer_edited_time ? times.length - 1 : 0].getAttribute('datetime'))
;
/* add styles */
if (time > since) {
comment.classList.add('hnc_new');
let elements = {
'comment': comment,
'text': comment.getElementsByClassName('usertext-body')[0].firstElementChild,
'time': comment.getElementsByClassName('live-timestamp')[0],
};
elements[this.config.apply_on].setAttribute('style', this.generate_comment_style(time, since));
}
}
},
reset_highlighting: function () {
let comments = document.getElementsByClassName('hnc_new');
for (let i = comments.length; i > 0; i--) {
let comment = comments[i - 1];
comment.classList.remove('hnc_new');
let elements = {
'comment': comment,
'text': comment.getElementsByClassName('usertext-body')[0].firstElementChild,
'time': comment.getElementsByTagName('time')[0],
};
for (let element in elements) {
elements[element].removeAttribute('style');
}
}
},
clear_history: function () {
let now = Date.now();
let expiration = this.config.history_expiration * 24 * 60 * 60 * 1000;
for (let thread in this.config.history) {
let visits = this.config.history[thread];
for (let i = 0; i < visits.length; i++) {
if (now - visits[i] > expiration) {
this.config.history[thread].splice(i);
if (!this.config.history[thread].length) {
delete this.config.history[thread];
}
}
}
}
},
generate_comment_style: function (comment_time, since) {
let style = this.config.comment_style;
style = style.replace(/\s+/g, ' ');
style = style.replace(/%color/g, this.get_color(Date.now() - comment_time, Date.now() - since));
return style;
},
get_color: function (comment_age, highlighting_since) {
if (!this.config.use_color_gradient) {
return this.config.color_newer;
}
if (comment_age > highlighting_since - 1) {
return this.config.color_older;
}
let time_diff = 1 - comment_age / highlighting_since,
color_newer = tinycolor(this.config.color_newer).toHsl(),
color_older = tinycolor(this.config.color_older).toHsl()
;
let color_final = tinycolor({
h: color_older.h + (color_newer.h - color_older.h) * time_diff,
s: color_older.s + (color_newer.s - color_older.s) * time_diff,
l: color_older.l + (color_newer.l - color_older.l) * time_diff,
});
return color_final.toHslString();
},
};
HNC.ui = {
create_comment_highlighter: function (visits) {
/* create element */
let highlighter = document.createElement('div');
highlighter.innerHTML = HNC.data.get('comment_highlighter');
highlighter.classList.add('rounded', 'gold-accent', 'comment-visits-box');
let commentarea = document.getElementsByClassName('commentarea')[0],
sitetable = commentarea.getElementsByClassName('sitetable')[0],
comment_margin = window.getComputedStyle(sitetable.firstChild).getPropertyValue('margin-left'),
gold_highlighter = document.getElementsByClassName('comment-visits-box')[0]
;
/* remove default comment highlighter */
if (gold_highlighter) {
gold_highlighter.parentNode.removeChild(gold_highlighter);
}
/* properly place */
highlighter.style.setProperty('margin-left', comment_margin);
commentarea.insertBefore(highlighter, sitetable);
/* generate visits */
let select = document.getElementById('comment-visits');
for (let visit of visits) {
let option = document.createElement('option');
option.textContent = time_ago(visit);
option.value = visit;
select.appendChild(option);
}
if (visits.length > 1) {
select.children[3].setAttribute('selected', '');
}
/* add listeners */
select.addEventListener('change', this.update_highlighting);
let custom = document.getElementById('hnc_custom_visit');
custom.style.setProperty('width', (select.getBoundingClientRect().width) + 'px');
custom.addEventListener('keydown', this.custom_visit_key_monitor);
custom.addEventListener('blur', this.set_custom_visit);
this.custom_pos = 0;
/* config button */
let config_button = document.getElementById('hnc_config_icon');
config_button.style.setProperty('background-image', HNC.data.get('config_icon').replace(/\s/g, ''));
config_button.addEventListener('click', this.show_config_dialog);
},
update_highlighting: function (event) {
/* no highlighting */
if (event.target.value == '') {
HNC.reset_highlighting();
}
/* custom */
else if (event.target.value == 'custom') {
document.getElementById('comment-visits').style.setProperty('display', 'none');
let custom = document.getElementById('hnc_custom_visit');
custom.style.removeProperty('display');
custom.focus();
custom.setSelectionRange(0, 2);
}
/* previous visit */
else {
HNC.reset_highlighting();
HNC.highlight(parseInt(event.target.value, 10));
}
},
custom_visit_key_monitor: function (event) {
if (event.altKey || event.ctrlKey || (event.shiftKey && event.key != 'Tab')) {
return;
}
if (event.key == 'Tab') {
let match = event.target.value.match(/^(\d+?:)\d+?$/);
if (match) {
if (event.shiftKey) {
HNC.ui.custom_pos--;
}
else {
HNC.ui.custom_pos++;
}
if (HNC.ui.custom_pos % 2 == 0) {
event.target.setSelectionRange(0, match[1].length - 1);
}
else {
event.target.setSelectionRange(match[1].length, match[0].length);
}
event.preventDefault();
event.stopPropagation();
}
}
else if (event.key == 'Enter') {
event.target.blur();
event.preventDefault();
event.stopPropagation();
}
},
set_custom_visit: function (event) {
let select = document.getElementById('comment-visits'),
match = event.target.value.match(/^(\d+?):(\d+?)$/)
;
if (match) {
let option = document.createElement('option'),
hours = parseInt(match[1], 10),
minutes = parseInt(match[2], 10),
visit = Date.now() - (hours * 60 + minutes) * 60 * 1000
;
option.value = visit;
option.textContent = time_ago(visit);
select.add(option, 2);
select.selectedIndex = 2;
}
else {
select.selectedIndex = 0;
}
let change = new Event('change');
select.dispatchEvent(change);
event.target.value = '00:00';
event.target.style.setProperty('display', 'none');
select.style.removeProperty('display');
},
create_config_dialog: function () {
/* create wrapper */
let wrapper = document.createElement('div');
document.body.appendChild(wrapper);
wrapper.id = 'hnc_dialog_wrapper';
wrapper.innerHTML = HNC.data.get('config_dialog');
/* add preview */
let comment_preview = document.getElementById('hnc_comment_preview');
let first_comment = document.getElementsByClassName('comment')[0].cloneNode(true);
first_comment.removeChild(first_comment.getElementsByClassName('child')[0]);
first_comment.style.setProperty('margin-left', '0');
comment_preview.appendChild(first_comment);
wrapper.style.setProperty('display', 'none');
wrapper.addEventListener('click', this.hide_config_dialog);
this.load_config_values();
this.add_listeners();
},
show_config_dialog: function () {
document.getElementById('hnc_dialog_wrapper').style.removeProperty('display');
/* disable scrolling */
//document.body.style.setProperty('top', -document.documentElement.scrollTop + 'px');
//document.body.style.setProperty('position', 'fixed');
//document.body.style.setProperty('overflow-y', 'scroll');
//document.body.style.setProperty('width', '100%');
},
hide_config_dialog: function (event) {
if (event.target.id != 'hnc_dialog_wrapper' && event.target.id != 'hnc_close_button') {
return;
}
let wrapper = document.getElementById('hnc_dialog_wrapper');
wrapper.style.setProperty('display', 'none');
/* enable scrolling */
//let scroll = -parseInt(document.body.style.top, 10);
//document.body.style.removeProperty('overflow-y');
//document.body.style.removeProperty('position');
//document.documentElement.scrollTop = scroll;
HNC.reset_highlighting();
HNC.highlight(parseInt(document.getElementById('comment-visits').value, 10));
HNC.cfg.save();
},
load_config_values: function () {
let dialog_settings = document.getElementsByClassName('hnc_setting');
for (let element of dialog_settings) {
let name = element.id.slice(4);
if (element.tagName == 'INPUT' && element.type == 'checkbox') {
element.checked = HNC.config[name];
if (element.dataset.disable) {
document.getElementById(element.dataset.disable).disabled = !element.checked;
}
}
else {
element.value = HNC.config[name];
}
}
this.update_preview();
},
add_listeners: function () {
let dialog_settings = document.getElementsByClassName('hnc_setting');
for (let element of dialog_settings) {
element.addEventListener('change', this.setting_change);
}
document.getElementById('hnc_clear_history_button').addEventListener('click', this.clear_all_history);
document.getElementById('hnc_reset_button').addEventListener('click', this.reset_config);
document.getElementById('hnc_close_button').addEventListener('click', this.hide_config_dialog);
},
setting_change: function (event) {
let name = event.target.id.slice(4);
if (event.target.tagName == 'INPUT' && event.target.type == 'text' && !event.target.validity.valid) {
event.target.value = HNC.config[name];
return;
}
if (event.target.tagName == 'INPUT' && event.target.type == 'checkbox') {
HNC.config[name] = event.target.checked;
if (event.target.dataset.disable) {
document.getElementById(event.target.dataset.disable).disabled = !event.target.checked;
}
}
else {
HNC.config[name] = event.target.value;
}
HNC.ui.update_preview();
},
reset_config: function (event) {
/* keep history */
let history = HNC.config.history;
/* reset */
HNC.config = HNC.cfg.default();
HNC.config.history = history;
HNC.ui.load_config_values();
},
clear_all_history: function (event) {
HNC.config.history = { };
},
update_preview: function () {
let preview = document.getElementById('hnc_comment_preview').firstElementChild;
let elements = {
'comment': preview,
'text': preview.getElementsByClassName('usertext-body')[0].firstElementChild,
'time': preview.getElementsByClassName('live-timestamp')[0],
};
for (let element in elements) {
elements[element].removeAttribute('style');
}
let comment_age = Date.parse(elements.time.getAttribute('dateTime')),
double_comment_age = comment_age - (Date.now() - comment_age) * 2
;
elements[HNC.config.apply_on].setAttribute('style', HNC.generate_comment_style(comment_age, double_comment_age));
},
};
HNC.data = {
comment_highlighter: function () {/*
<div class="title" style="line-height: 20px;">Highlight comments since:
<select id="comment-visits">
<option value="">no highlighting</option>
<option value="custom">custom</option>
</select>
<input id="hnc_custom_visit" type="text" value="00:00" pattern="\d+?:\d+?" style="display: none;" />
<span id="hnc_config_icon"></span>
</div>
*/},
config_dialog: function () {/*
<div id="hnc_dialog">
<div>
<label><input id="hnc_prefer_edited_time" class="hnc_setting" type="checkbox">Highlight edited comments</label>
</div>
<hr />
<div>
<label><input type="checkbox" id="hnc_use_color_gradient" class="hnc_setting" data-disable="hnc_color_older">Use time based color gradient</label>
</div>
<div>
<label class="hnc_fixed_width" for="hnc_color_newer">Newer comments color</label><input type="text" id="hnc_color_newer" class="hnc_setting" title="Supported formats: #80bfff rgba(128, 191, 255, 1) hsla(210, 100%, 75%, 1)" pattern="(#(?:[\da-fA-F]{3}){1,2}|rgb\((?:\d{1,3},\s*){2}\d{1,3}\)|rgba\((?:\d{1,3},\s*){3}\d*\.?\d+\)|hsl\(\d{1,3}(?:,\s*\d{1,3}%){2}\)|hsla\(\d{1,3}(?:,\s*\d{1,3}%){2},\s*\d*\.?\d+\))">
</div>
<div>
<label class="hnc_fixed_width" for="hnc_color_older">Older comments color</label><input type="text" id="hnc_color_older" class="hnc_setting" title="Supported formats: #cce5ff rgba(204, 229, 255, 1) hsla(210, 100%, 90%, 1)" pattern="(#(?:[\da-fA-F]{3}){1,2}|rgb\((?:\d{1,3},\s*){2}\d{1,3}\)|rgba\((?:\d{1,3},\s*){3}\d*\.?\d+\)|hsl\(\d{1,3}(?:,\s*\d{1,3}%){2}\)|hsla\(\d{1,3}(?:,\s*\d{1,3}%){2},\s*\d*\.?\d+\))">
</div>
<hr />
<div>
<label class="hnc_fixed_width" for="hnc_apply_on">Apply styles on</label><select id="hnc_apply_on" class="hnc_setting"><option>text</option><option>comment</option><option>time</option></select>
</div>
<div>
<label for="hnc_comment_style">Comment style</label>
<textarea id="hnc_comment_style" class="hnc_setting"></textarea>
</div>
<hr />
<div>
<label for="hnc_comment_preview">Preview</label>
<div id="hnc_comment_preview"></div>
</div>
<hr />
<div style="float: right">
<button id="hnc_clear_history_button">Clear history</button>
<button id="hnc_reset_button">Reset</button>
<button id="hnc_close_button">Close</button>
<div>
</div>
*/},
config_style: function () {/*
input.hnc_setting[pattern]:invalid, #hnc_custom_visit:invalid {
box-shadow: 0 0 5px 0 #FF4060;
background-color: #FF4060;
}
#hnc_config_icon {
display: inline-block;
width: 20px;
height: 20px;
vertical-align: top;
}
#hnc_dialog_wrapper {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
width: 100%;
height: 100%;
z-index: 2147483647;
background-color: rgba(192, 192, 192, 0.7);
font-size: 12px;
}
#hnc_dialog {
align-self: flex-start;
margin-top: 80px;
padding: 5px 0;
width: 900px;
max-height: 95%;
overflow-y: auto;
box-shadow: 0 0 20px 5px rgb(64, 64, 64);
background-color: #F5F5F5;
}
#hnc_dialog > div {
margin: 5px 10px;
}
#hnc_dialog > hr {
margin: 0;
height: 1px;
border: none;
background-color: grey;
}
label.hnc_fixed_width {
width: 148px;
}
#hnc_dialog label {
display: inline-block;
}
#hnc_dialog label > input:not([type=checkbox]) {
margin-left: 5px;
}
#hnc_dialog label > input[type=checkbox] {
margin-right: 5px;
vertical-align: top;
}
#hnc_comment_style {
box-sizing: border-box;
width: 100% !important;
height: 50px;
max-height: 400px;
font-family: monospace;
}
*/},
config_icon: function () {/*
url(data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxIAAAsSAdLdfvwA
AAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNvyMY98AAAIfSURBVDhPnZQ9SBxRFIXHaIgg2Agm2GhhofiDsCAYiwV1ZQnM/rIGUmStFtLY
hkjCdoKKtU1S2PmDyGLAThAEC8HKQhsFFSwFE0yCuua7b+9bXtbZoB44zLxzzr3z3ps34/0P+Xz+WSKRKMAbYTKZXFXraYjH48M0KcI7ZTGVSg2q
/XAwmyY4C/84zSx/on+ORCINGg8GoQEYomACXjgNqvGEFbzj2k1dv7YpAWEMw12aIfoVLHA/BWe43+R6bX2HshVvTbNoNPqC4GlAaD2TybwyIQfp
dLqL/F5lHu1MeplAgFmgWa32uAcKG8nsV9Yxyw7xaxgsWZHgD67NphLIU9krH74Jh8N1KssJeE2uvE3ULSDXGJPBB2vAZSOCUCj0HG/bKdpALhWV
JnLoeCnVTcNP1oBfVBY97OiGzKxPbfEXHe+9yp7HYNIahPIqS4Eco3IzWPR9v11tqVu2HtmsysaQs2eNgsoCWdY8LKLfwmnVzWeJfmzrysdG3ibB
79aAvzBbjamIxWItzOylDg2oGXVqZCIr5mSwJz2uoeZWNput19p7kPNJrjw7p67Ty+Vy8iaPAswdeZj2sJAtGArKwyPpZVIs0Sckv6d/Qmi3XHfh
N+4XuB5Yz6XWxkwzC76YXoxOjI/w0oarkew5ExlnFW2Mu7VNMGSPKPhKMOhH8BvOyeen8YeDp/sVzeSvMqL246FnbV32SPdpTa0q8Ly/60amOe0Z
Tw0AAAAASUVORK5CYII=)
*/},
get: function (name) {
return this.function_to_string(this[name]);
},
/* original authored by lavoiesl, at https://gist.github.com/lavoiesl/5880516*/
function_to_string: function (func, strip_leading_whitespace) {
if (strip_leading_whitespace === undefined) {
strip_leading_whitespace = 1;
}
let matches = func.toString().match(/function[\s\w]*?\(\)\s*?\{[\S\s]*?\/\*\!?\s*?\n([\s\S]+?)\s*?\*\/\s*\}/);
if (!matches) {
return false;
}
if (strip_leading_whitespace) {
matches[1] = matches[1].replace(/^(\t| {4})/gm, '');
}
return matches[1];
}
};
HNC.cfg = {
load: function () {
let config = GM_getValue('config');
if (!config) {
return this.default();
}
return JSON.parse(config);
},
save: function () {
GM_setValue('config', JSON.stringify(HNC.config));
},
default: function () {
return {
'prefer_edited_time': 1,
'use_color_gradient': 1,
'color_newer': 'hsl(210, 100%, 65%)',
'color_older': 'hsl(210, 100%, 90%)',
'apply_on': 'text',
'comment_style': 'background-color: %color !important;\npadding: 0 5px;',
'history': { },
'history_expiration': 7, /* in days */
};
},
}
/* original authored by TheBrain, at http://stackoverflow.com/a/12475270 */
function time_ago(time, precision) {
if (precision == undefined) {
precision = 2;
}
switch (typeof time) {
case 'number': break;
case 'string': time = +new Date(time); break;
case 'object': if (time.constructor === Date) time = time.getTime(); break;
default: time = +new Date();
}
let time_formats = [
[ 60, 'seconds', 1], // 60
[ 120, '1 minute', '1 minute from now'], // 60*2
[ 3600, 'minutes', 60], // 60*60, 60
[ 7200, '1 hour', '1 hour from now'], // 60*60*2
[ 86400, 'hours', 3600], // 60*60*24, 60*60
[ 172800, '1 day', 'Tomorrow'], // 60*60*24*2
[ 604800, 'days', 86400], // 60*60*24*7, 60*60*24
[ 1209600, '1 week', 'Next week'], // 60*60*24*7*4*2
[ 2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
[ 4838400, '1 month', 'Next month'], // 60*60*24*7*4*2
[ 29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
[ 58060800, '1 year', 'Next year'], // 60*60*24*7*4*12*2
[ 2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
];
let seconds = (+new Date() - time) / 1000;
if (seconds < 2) {
return 'just now';
}
let durations = [ ];
while (1) {
let i = 0,
format;
while (format = time_formats[i++]) {
if (seconds < format[0]) {
if (typeof format[2] == 'string') {
durations.push(format[1]);
break;
}
else {
durations.push(Math.floor(seconds / format[2]) + ' ' + format[1]);
break;
}
}
}
if (i > time_formats.length) {
return 'a very long time ago';
}
if (typeof time_formats[i - 1][2] == 'string') {
seconds -= time_formats[i][2];
}
else {
seconds -= Math.floor(seconds / time_formats[i - 1][2]) * time_formats[i - 1][2];
}
if (precision > i && durations.length > 1) {
durations.pop();
break;
}
if (seconds == 0) {
break;
}
}
let result;
result = durations.slice(-2).join(' and ') + ' ago';
durations = durations.slice(0, -2);
if (durations.length) {
durations.push(result);
result = durations.join(', ');
}
return result;
}
try {
HNC.init();
}
catch (error) {
console.log(error);
}