- // ==UserScript==
- // @name Hackernews Modern
- // @namespace sagiegurari
- // @version 1.9
- // @author Sagie Gur-Ari
- // @description Improved mobile usability and modern styling for Hackernews
- // @homepage https://github.com/sagiegurari/userscripts-hackernews
- // @supportURL https://github.com/sagiegurari/userscripts-hackernews/issues
- // @match https://news.ycombinator.com/*
- // @match https://hckrnews.com/*
- // @match https://hackerweb.app/*
- // @grant none
- // @license MIT License
- // ==/UserScript==
-
- (function run() {
- 'use strict';
-
- const isAndroid = navigator.userAgent.toLowerCase('android') !== -1;
- const isEmulator = isAndroid && !navigator.userAgentData.mobile;
- const mobileOrEmulator = isEmulator || navigator.userAgentData.mobile;
-
- const isDebug = isEmulator;
- const logDebug = param => {
- console.log('[DEBUG]', param);
- };
-
- const element = document.createElement('style');
- element.type = 'text/css';
- document.head.appendChild(element);
- const styleSheet = element.sheet;
-
- const ycombinatorDomain = window.location.hostname.indexOf('.ycombinator.com') !== -1;
- const hckrnewsDomain = !ycombinatorDomain && window.location.hostname.indexOf('hckrnews.com') !== -1;
- const hackerwebDomain = !ycombinatorDomain && window.location.hostname.indexOf('hackerweb.app') !== -1;
-
- const articlePage = (ycombinatorDomain && window.location.search.indexOf('id=') !== -1) || (hackerwebDomain && window.location.href.indexOf('/#/item/') !== -1);
-
- logDebug({
- platform: {
- isAndroid,
- isEmulator,
- mobileOrEmulator
- },
- isDebug,
- articlePage,
- domain: {
- ycombinatorDomain,
- hckrnewsDomain,
- hackerwebDomain
- }
- });
-
- const addRules = (rules) => {
- rules.forEach(cssRule => {
- styleSheet.insertRule(cssRule, styleSheet.cssRules.length);
- });
- };
-
- const cssRules = [
- // defaults
- '.subtext .age a[href^="item"] { color: #828282; }',
-
- // colors
- '#hnmain tr:first-child td, .comment-tree { background-color: #333; }',
- 'html, body, #hnmain, #hnmain table.itemList tr:first-child td { background-color: #222; }',
- 'a:link, .subtext a[href^="item"]:not(:visited), a:link.togg.clicky, .commtext, .comment-tree a[rel="nofollow"], .comment-tree .reply a { color: #eee; }',
- '.visited a.titlelink, .visited a:link, .visited .subtext a[href^="item"] { color: #888; }',
- '.morelink { text-align: center; display: block; margin: 10px 40px 10px 0; background-color: #af4000; font-weight: bold; padding: 10px; }',
- ];
-
- // if mobile or emulator
- if (mobileOrEmulator) {
- cssRules.push(...[
- // styles
- '.pagetop { font-size: 16pt; }',
- '.title { font-size: 14pt; }',
- '.comhead { font-size: 12pt; }',
- '.subtext { font-size: 0; padding: 5px 0; }',
- '.subtext span { padding: 0 2px; }',
- '.subtext span, .subtext a:not([href^="item"]), .subtext .age a[href^="item"] { font-size: 12pt; text-decoration: none; }',
- '.subtext a[href^="item"] { font-size: 14pt; text-decoration: underline; }',
- '.subtext a[href^="hide"] { display: none; }',
- '.default { font-size: 12pt }',
- ]);
- }
-
- if (hckrnewsDomain) {
- cssRules.push(...[
- 'body, a:hover, a, .points, .comments { color: #eee; }',
- 'body .entries a:hover, body .nav > li > a:hover { background-color: #333; }',
- '.form-actions { background-color: #222 }',
- ]);
- } else if (hackerwebDomain && articlePage) {
- cssRules.push(...[
- '.view > header, body header, body .grouped-tableview, .post-content, body .view.view.view section { background-color: #333; }',
- 'body .view .post-content pre, body .view section.comments pre { background-color: #222 }',
- 'body .view .post-content header h1, body p, body pre, .view section.comments button.comments-toggle, body li { color: #eee; }',
- '.view section.comments button.comments-toggle, .view section.comments button.comments-toggle:hover { background-color: #555 }',
- ]);
- }
-
- addRules(cssRules);
-
- if (articlePage) {
- // collapse non top comments
- document.querySelectorAll('.ind:not([indent="0"])').forEach(topCommentIndent => {
- topCommentIndent.parentElement.querySelectorAll('.togg.clicky').forEach(toggle => toggle.click());
- });
-
- // remove root/next/prev links
- addRules([
- '.navs .clicky:not(.togg) { display: none; }',
- ]);
- } else {
- const storage = window.localStorage;
- if (storage && typeof storage.getItem === 'function') {
- const KEY = 'hn-cache-visited';
- const CACHE_LIMIT = 1000;
-
- const readFromCache = () => {
- const listStr = storage.getItem(KEY);
-
- if (!listStr) {
- return [];
- }
-
- return listStr.split(',');
- };
- const writeToCache = (ids) => {
- if (!ids || !Array.isArray(ids) || !ids.length) {
- return;
- }
-
- // add to start
- cache.unshift(...ids);
-
- // remove duplicates
- const seen = {};
- cache = cache.filter(function (item) {
- if (seen[item]) {
- return false;
- }
-
- seen[item] = true;
- return true;
- });
-
- // trim
- const extraCount = cache.length - CACHE_LIMIT;
- if (extraCount) {
- cache.splice(cache.length - extraCount, extraCount);
- }
-
- storage.setItem(KEY, cache.join(','));
- };
-
- let cache = readFromCache();
-
- const entryRowSelector = ycombinatorDomain ? 'tr.athing' : '.entry.row';
- const linkSelector = ycombinatorDomain ? 'tr.visited + tr' : '.entry.row .link.story';
-
- // mark visited
- const markVisited = () => {
- document.querySelectorAll(entryRowSelector).forEach(element => {
- if (cache.indexOf(element.id) !== -1) {
- element.classList.add('visited');
- }
- });
-
- document.querySelectorAll(linkSelector).forEach(element => {
- element.classList.add('visited');
- });
- };
- markVisited();
-
- // listen to scroll and add to cache
- const markVisibleAsVisited = () => {
- const elements = document.querySelectorAll(`${entryRowSelector}:not(.visited)`);
-
- let started = false;
- const ids = [];
- for (let index = 0; index < elements.length; index++) {
- const element = elements[index];
- const bounding = element.getBoundingClientRect();
- if (bounding.top >= 0 &&
- bounding.bottom <= window.innerHeight) {
- started = true;
- ids.push(element.id);
- } else if (started) {
- break;
- }
- }
-
- if (ids.length) {
- writeToCache(ids);
- }
- };
- let timeoutID = null;
- document.addEventListener('scroll', () => {
- clearTimeout(timeoutID);
- timeoutID = setTimeout(markVisibleAsVisited, 25);
- }, {
- passive: true
- });
-
- markVisibleAsVisited();
- }
- }
- }());