NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Reddit fade seen links
// @namespace https://github.com/Farow/userscripts
// @description Fades links that you have already seen
// @include /https?:\/\/[a-z]+\.reddit\.com\//
// @include https://news.ycombinator.com/*
// @include https://lobste.rs/*
// @include https://openuserjs.org/*
// @include http*://www.producthunt.com/*
// @include https://www.qudos.io/*
// @include https://news.layervault.com/
// @version 1.1.3
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// ==/UserScript==
'use strict';
/*
changelog:
2017-07-08 - 1.1.3
- fixed reddit parent selector
2015-09-16 - 1.1.2
- fixed hacker news selector (authored by Poorchop)
2015-08-08 - 1.1.1
- fixed issue where the userscript wouldn't work if there were no previous stored data
2015-05-23 - 1.1.0
- added support for producthunt.com, qudos.io, news.layervault.com
2015-05-22 - 1.0.9
- fixed hacker news issue
2014-11-14 - 1.0.8
- fixed issue where you had to click twice to open a link with fade_mode = 3
2014-11-04 - 1.0.7
- hide seen button now displays the amount of seen/faded links
- hide seen button and menu entries are now only created if links are found
- right clicks and modifier keys will now prevent links being marked as seen when hovering over links
2014-10-01 - 1.0.6
- duplicate links are now marked on hover/click
- seen links are now saved properly
- hide seen button is only added if links are found
- custom styles are only applied once
- the transition effect now only applies to new links
- improved support for openuserjs.org
2014-09-19 - 1.0.5
- added live feedback on hover/click
- added clear all on this page command
- added fade_mode option
- improved hiding non-script links on openuserjs.org
- removed opacity option
2014-09-14 - 1.0.4
- added a hide seen button
- improved support for hacker news
2014-09-13 - 1.0.3
- added support for openuserjs.org
- added support for styling new links
- moved some options to css style to allow easier styling
- added remove styles command
- clear last now only works for the links that you saw on your last page,
not all the links that you have seen once
2014-09-06 - 1.0.2
- added support for news.ycombinator.com and lobste.rs
- old links are now removed from storage depending on their last visit time, not first visit time
2014-09-02 - 1.0.1 - no longer fades links or comments on a profile page
2014-09-02 - 1.0.0 - initial release
*/
let fade_mode = 2, /* 1: automatically fade all links, 2: fade hovered links, 3: fade clicked links */
hide_after = 0, /* times seen a link before hiding it (0 to never hide links) */
expiration = 14, /* time after which to remove old links from storage, in days */
style =
'.fade { opacity: 0.5; }'
+ '.hide { display: none; }'
+ '.new { box-shadow: -2px 0px 0px 0px hsl(210, 100%, 75%); }'
+ '.new.fade { box-shadow: none; transition: all 1s ease-in; }'
+ '.new.dupe { box-shadow: -2px 0px 0px 0px hsl(0, 100%, 75%); }'
;
window.addEventListener('load', init); /* compatibility with scripts that modify links */
window.addEventListener('unload', save_new);
let new_links = [ ],
old_links = 0,
site,
hide_button,
rules = {
'news.ycombinator.com': {
'links': function () {
/* exclude 'more' and comment pages */
return [].slice.call(document.querySelectorAll('td.title > a'), 0, -1);
},
'parents': function (link) {
return [
link.parentElement.parentElement,
link.parentElement.parentElement.nextElementSibling,
link.parentElement.parentElement.nextElementSibling.nextElementSibling
];
},
'hide_button': function () {
let a = document.createElement('a');
a.href = '#';
a.textContent = 'hide seen';
document.getElementsByClassName('pagetop')[0].innerHTML += ' | ';
return ['.pagetop', a];
},
'hide_button_set_amount': function (button, amount) {
button.textContent = 'hide seen (' + amount + ')';
},
},
'reddit.com': {
'include': function () {
return document.body.classList.contains('listing-page');
},
'exclude': function () {
return document.body.classList.contains('profile-page');
},
'links': '.thing.link > .entry a.title',
'parents': function (link) {
return [ link.closest('.thing') ];
},
'style': '.fade, .dupe { overflow: hidden; }',
'hide_button': function () {
let li = document.createElement('li'),
a = document.createElement('a');
a.href = '#';
a.textContent = 'hide seen';
a.classList.add('choice');
li.appendChild(a);
return [ '.tabmenu', li ];
},
'hide_button_set_amount': function (button, amount) {
button.children[0].textContent = 'hide seen (' + amount + ')';
},
},
'lobste.rs': {
'exclude': function () {
return document.querySelector('.comments');
},
'links': '.link a',
'parents': function (link) {
return [ link.parentNode.parentNode.parentNode ];
},
'hide_button': function () {
let a = document.createElement('a');
a.href = '#';
a.textContent = 'Hide seen';
return ['.headerlinks', a];
},
'hide_button_set_amount': function (button, amount) {
button.textContent = 'hide seen (' + amount + ')';
},
},
'openuserjs.org': {
'links': 'a.tr-link-a',
'parents': function(link) {
if (link.parentNode.tagName === 'B') {
link = link.parentNode;
}
return [ link.parentNode.parentNode ];
},
'hide_button': function (){
let li = document.createElement('li'),
a = document.createElement('a');
a.href = '#';
a.textContent = 'Hide seen';
li.appendChild(a);
return ['ul.navbar-right', li, document.getElementsByClassName('navbar-right')[0].lastElementChild.previousElementSibling];
},
'hide_button_set_amount': function (button, amount) {
button.children[0].textContent = 'hide seen (' + amount + ')';
},
'style': '.table-responsive { overflow-x: visible; }',
},
'producthunt.com': {
'links': '.post-url',
'parents': function (link) {
return [ link.parentElement.parentElement.parentElement ];
},
'hide_button': function () {
let a = document.createElement('a');
a.href = '#';
a.textContent = 'hide seen';
a.classList.add('header--secondary--link');
return ['.v-links > .container', a];
},
'hide_button_set_amount': function (button, amount) {
button.textContent = 'hide seen (' + amount + ')';
},
'style': '.new { border-radius: 0; }',
},
'qudos.io': {
'links': '.entry .title',
'parents': function (link) {
return [ link.parentElement.parentElement.parentElement.parentElement.parentElement ];
},
},
'news.layervault.com': {
'links': '.StoryUrl',
'parents': function (link) {
return [ link.parentElement ];
},
'style': '.fade { opacity: 1; } .fade .StoryUrl { opacity: 0.5; }',
},
}
;
function init() {
for (site in rules) {
let site_tokens = site.split('.'),
domain_tokens = location.hostname.split('.').slice(-site_tokens.length);
if (equal_arrays(site_tokens, domain_tokens)) {
site = rules[site];
break;
}
}
if (site === undefined) {
return;
}
if (site.hasOwnProperty('include') && !site.include()) {
return;
}
if (site.hasOwnProperty('exclude') && site.exclude()) {
return;
}
if (!site.hasOwnProperty('remove_default_styles')) {
GM_addStyle(style);
}
if (site.hasOwnProperty('style')) {
GM_addStyle(site.style);
}
let found = check_links(site);
if (found) {
if (site.hasOwnProperty('hide_button')) {
let [where, button, insert_before] = site.hide_button();
let element = document.querySelector(where);
if (element) {
if (insert_before) {
element.insertBefore(button, insert_before);
}
else {
element.appendChild(button);
}
button.addEventListener('click', function (event) {
check_links(site, 1);
event.preventDefault();
});
hide_button = button;
update_hide_button();
}
}
GM_registerMenuCommand('Fade links: clear all', clear.bind(undefined, 0, site));
GM_registerMenuCommand('Fade links: clear all on this page', clear.bind(undefined, 1, site));
GM_registerMenuCommand('Fade links: clear last', clear.bind(undefined, 2, site));
GM_registerMenuCommand('Fade links: remove styles', remove_styles.bind(undefined, site));
GM_registerMenuCommand('Fade links: hide seen', check_links.bind(undefined, site, 1));
}
remove_old();
}
function check_links(site, on_demand_hide) {
let old = get_links_in_storage(),
links = get_links_in_page(site);
links.forEach(function (element) {
let url = element.href;
if (on_demand_hide) {
if (get_parents(site, element)[0].classList.contains('fade')) {
fade(site, element, 0, 0, 1); /* force */
}
old_links = 0;
update_hide_button();
return;
}
if (!old.hasOwnProperty(url)) {
/* mark new */
let parents = get_parents(site, element);
for (let i = 0; i < parents.length; i++) {
parents[i].classList.add('new');
}
/* automatically add to history */
if (fade_mode === 1) {
new_links.push(url);
if (!old.hasOwnProperty(url)) {
old[url] = {
seen: 0,
last: 1,
when: Date.now(),
accessed: 1,
};
}
}
/* add to history on hover/click */
else {
let capture_event = fade_mode === 2 ? 'mouseover' : 'mouseup';
element.addEventListener(capture_event, function (e) {
/* avoid right click and modifier buttons */
if (e.button === 2 || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
return;
}
new_links.push(e.currentTarget.href);
mark_dupes(site, links, e.currentTarget);
fade(site, element, 1);
old_links++;
update_hide_button();
/* clone hack to remove event listener */
window.setTimeout(function (target) {
let clone = target.cloneNode(1);
target.parentNode.replaceChild(clone, target);
}, 0, e.currentTarget);
});
}
}
else {
old[url].when = Date.now();
old_links++;
if (old[url].accessed) {
fade(site, element, old[url].seen, 1);
return;
}
old[url].accessed = 1;
old[url].seen++;
if (old[url].last == 1) {
old[url].last++;
}
else if (old[url].last) {
old[url].last = 0;
}
fade(site, element, old[url].seen);
}
});
save_links(old);
return links.length;
}
function clear(clear_type, site) {
/* clear all */
if (!clear_type) {
save_links({});
return;
}
let links = get_links_in_storage();
/* clear all on this page */
if (clear_type === 1) {
let new_links = get_links_in_page(site);
new_links.forEach(function (element) {
let url = element.href;
if (links.hasOwnProperty(url)) {
delete links[url];
}
});
}
/* clear last */
else if (clear_type === 2) {
for (let url in links) {
if (links[url].last == 2) {
delete links[url];
}
}
}
save_links(links);
return;
}
function fade(site, link, seen, is_dupe, force_hide) {
let parents = get_parents(site, link);
if (force_hide || (hide_after !== 0 && seen > hide_after - 1)) {
for (let i = 0; i < parents.length; i++) {
parents[i].classList.add('hide');
}
return;
}
for (let i = 0; i < parents.length; i++) {
if (seen) {
parents[i].classList.add('fade');
}
else if (is_dupe) {
parents[i].classList.add('dupe');
}
}
}
function update_hide_button() {
if (site.hasOwnProperty('hide_button_set_amount')) {
site.hide_button_set_amount(hide_button, old_links);
}
}
function mark_dupes(site, links, exception) {
/* already marked as a dupe */
if (get_parents(site, exception)[0].classList.contains('dupe')) {
return;
}
links.forEach(function (link) {
if (link === exception) {
return;
}
if (link.href === exception.href) {
fade(site, link, 0, 1); /* add dupe class */
}
});
}
function save_new() {
let old = get_links_in_storage();
new_links.forEach(function (url) {
if (!old.hasOwnProperty(url)) {
old[url] = {
seen: 1,
last: 1,
when: Date.now(),
accessed: 1,
};
}
});
save_links(old);
}
function remove_styles(site) {
let links = get_links_in_page(site);
links.forEach(function (element) {
let parents = get_parents(site, element);
for (let i = 0; i < parents.length; i++) {
parents[i].classList.remove('fade', 'dupe', 'hide', 'new');
}
});
}
function remove_old() {
let links = get_links_in_storage(),
diff = Date.now() - expiration * 86400000, /* expiration * 1 day */
count = 0;
for (let url in links) {
if (links[url].when < diff) {
delete links[url];
count++;
}
}
if (count) {
save_links(links);
}
}
function get_links_in_page(site) {
if (typeof site.links == 'function') {
return site.links();
}
return [].slice.call(document.querySelectorAll(site.links));
}
function get_links_in_storage() {
let links = GM_getValue('links');
if (!links) {
return { };
}
return JSON.parse(links);
}
function get_parents(site, link) {
if (site.hasOwnProperty('parents')) {
return site.parents(link);
}
return link;
}
function save_links(links) {
for (let url in links) {
if (links[url].accessed) {
delete links[url].accessed;
}
else if (links[url].last == 2) {
links[url].last = 0;
}
}
GM_setValue('links', JSON.stringify(links));
}
function equal_arrays(a, b) {
if (a === b) {
return true;
}
if (!a || !b || a.length != b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}