reorx hackernews tweak

1/20/2024, 11:14:57 AM

// ==UserScript==
// @name        reorx hackernews tweak
// @namespace   Violentmonkey Scripts
// @match       https://news.ycombinator.com/*
// @grant       GM_addStyle
// @version     1.1
// @author      Reorx
// @license MIT
// @description 1/20/2024, 11:14:57 AM
// ==/UserScript==

const linksPanelClass = 'links-panel';

GM_addStyle(`
:root {
  --hn-font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol',sans-serif;
/*   --hn-font-family: Verdana, Geneva, sans-serif; */
  --hn-font-family-mono: monospace;
  --hn-font-size: 15px;
  --hn-font-size-mono: 13px;
  --hn-gray-9: #131313;
  --hn-signature: #ff6600;
}

/* layout */
#hnmain {
  width: 1080px !important;
}

/* typography */
html body  { font-family: var(--hn-font-family); font-size: var(--hn-font-size); }
#hnmain {
  /* override */
  td    { font-family: var(--hn-font-family); font-size: var(--hn-font-size);  }

  .admin td   { font-family: var(--hn-font-family); font-size: var(--hn-font-size);  }
  .subtext td { font-family: var(--hn-font-family); font-size:   var(--hn-font-size);  }

  input    { font-family: var(--hn-font-family-mono); font-size: var(--hn-font-size-mono); }
  input[type=submit] { font-family: var(--hn-font-family); margin-top: -20px; }
  textarea { font-family: var(--hn-font-family-mono); font-size: var(--hn-font-size-mono);  }

  a:link    {  text-decoration:none; }
  a:visited {  text-decoration:none; }
  a:hover { text-decoration: underline; }

  .default { font-family: var(--hn-font-family); font-size:  var(--hn-font-size); }
  .admin   { font-family: var(--hn-font-family); font-size: var(--hn-font-size); }
  .title   { font-family: var(--hn-font-family); font-size:  calc(var(--hn-font-size) + 2px); }
  .subtext { font-family: var(--hn-font-family); font-size:   calc(var(--hn-font-size) - 2px); padding-bottom: 2px; }
  .yclinks { font-family: var(--hn-font-family); font-size:   var(--hn-font-size); }
  .pagetop { font-family: var(--hn-font-family); font-size:  var(--hn-font-size); }

  .toptext { color: var(--hn-gray-9); padding-top: 8px; }
  .comhead { font-family: var(--hn-font-family); font-size:   calc(var(--hn-font-size) - 2px); }
  .comment { font-family: var(--hn-font-family); font-size:   var(--hn-font-size); color: var(--hn-gray-9); }
  /* highlight link in user text */
  .toptext a, .commtext a { color: blue; }
  .reply {
    u { text-decoration: none; }
    a { color: #828282; }
  }

  /* newly added */
  .titleline a { color: var(--hn-gray-9); }
  .comment pre { font-size: var(--hn-font-size-mono); }
}

/* elements */
.titleline {
  display: block;
  margin-bottom: 2px;
}

textarea {
  display: block;
  height: 3em;
}

.comment-tree {
  margin-top: -25px;
}

.score, .hnuser {
  color: var(--hn-signature);
}

/* links panel */

.${linksPanelClass} {
  position: fixed;
  right: 12px;
  bottom: 0;
  width: 430px;
  background: #fff;
  border: 1px solid var(--hn-signature);
  border-bottom: 0;
  display: flex;
  flex-direction: column;

  .title {
    font-size: 1.2em;
    padding: 6px 12px;
    color: var(--hn-signature);
    border-bottom: 1px solid var(--hn-signature);
    background: rgba(255, 102, 0, .1);
    flex-shrink: 0;
    cursor: pointer;
  }
  .links {
    overflow-y: auto;
    padding: 8px 12px;
    max-height: 600px;
  }

  .link-item {
    margin-bottom: 8px;
  }
  .url {
    color: #000;
    padding: 2px 0;
    &:hover {
      color: blue;
    }
  }
  .url,
  .comment-item {
    display: inline-block;
    width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .comments {
    .comment-item {
      padding: 2px 0 2px 12px;
      border-left: 1px solid transparent;
      cursor: pointer;
      color: #828282;
    }
    .comment-item:hover {
      border-color: #aaa;
      background: #eee;
    }
  }
}
`);

const sublineLinks = document.querySelectorAll('.subline > a');
// comments page does not have .subline
if (sublineLinks.length > 0) {
  const commentsCountLink = sublineLinks[sublineLinks.length - 1];
  commentsCountLink.style.color = 'var(--hn-signature)';
}


function createEl(tagName, {innerText, className, id, attrs}, appendTo) {
  const el = document.createElement(tagName)
  if (innerText) el.innerText = innerText;
  if (className) el.className = className;
  if (id) el.id = id;
  if (attrs) {
    for (const [k, v] of Object.entries(attrs)) {
      el.setAttribute(k, v);
    }
  }
  if (appendTo) appendTo.appendChild(el);
  return el;
}

/* collect links in comments */
function initLinksPanel() {
  // loop links
  const ignoredHosts = ['localhost','127.0.0.1', '0.0.0.0'];
  const linkCommentsMap = {};
  document.querySelectorAll('.commtext a:not([href^="reply?"])').forEach(a => {
    const url = a.href;
    if (!url) return;
    const urlObj = new URL(url);
    if (ignoredHosts.includes(urlObj.hostname)) return;
    if (!(url in linkCommentsMap)) {
      linkCommentsMap[url] = []
    }
    const comments = linkCommentsMap[url];

    // create comment object
    const comtr = a.closest('.comtr');
    let commtext = comtr.querySelector('.commtext');
    if (commtext.querySelector('.reply')) {
      commtext = commtext.cloneNode(true);
      commtext.querySelector('.reply').remove()
    }
    const comment = {
      id: comtr.id,
      username: comtr.querySelector('.hnuser').textContent,
      age: comtr.querySelector('.age').textContent,
      content: commtext.textContent.slice(0, 100).trim(),
    }
    // push only if not exists
    if (!comments.find(c => c.id === comment.id)) {
      comments.push(comment);
    }
  })

  // cancel the function if no links found
  const linksCount = Object.keys(linkCommentsMap).length;
  if (linksCount === 0) {
    return
  }

  // create links panel elements
  const panel = createEl('div', {
    className: linksPanelClass,
  }, document.body);
  // title
  const titleEl = createEl('div', {
    className: 'title',
    innerText: `Links in comments (${linksCount})`,
    title: 'click to toggle the links panel'
  }, panel)
  // links
  const linksEl = createEl('div', { className: 'links', }, panel);

  // click titleEl to hide linksEl
  titleEl.addEventListener('click', () => {
    linksEl.style.display = linksEl.style.display === 'none' ? 'block' : 'none';
  })

  const createLinkEl = (url, comments) => {
    const el = createEl('div', { className: 'link-item', }, linksEl);
    const urlEl = createEl('a', {
      className: 'url',
      // remove url schema
      innerText: url.replace(/^https?:\/\//, ''),
      attrs: {
        href: url,
        target: '_blank',
      }
    }, el);
    const commentsEl = createEl('div', { className: 'comments', }, el);
    comments.forEach(comment => {
      const commentEl = createEl('a', {
        // NOTE clicky is a HN built-in class that will help to jump to the comment without changing the url
        className: 'comment-item clicky',
        innerText: `@${comment.username}: ${comment.content}`,
        attrs: { href: `#${comment.id}`, title: comment.age, },
      }, commentsEl);
      commentsEl.appendChild(commentEl);
    });
    return el;
  };

  // sort links by comments length and then loop the links to create elements
  Object.entries(linkCommentsMap).sort(
    (a, b) => b[1].length - a[1].length
  ).forEach(i => {
    const [url, comments] = i;
    createLinkEl(url, comments);
  });

}

// run initLinksPanel only if url matches https://news.ycombinator.com/item?id=…
if (/news\.ycombinator\.com\/item\?id=\d+/.test(location.href)) {
  initLinksPanel();
}

// add submitted link after submit link
const usernameLink = document.querySelector('#me');
const username = usernameLink.textContent;

// Find the submit link
const submitLink = document.querySelector('a[href="submit"]');
const linksContainer = submitLink.parentElement;

// Create the new submitted link
const dimmedColor = 'rgb(186 75 0)';

function newSubLink(href, text) {
    const sl = document.createElement('a');
    sl.href = href;
    sl.textContent = text;
    sl.style.color = dimmedColor;
    return sl
}

function separatorNode() {
    const sp = document.createElement('span');
    sp.textContent = ' | ';
    sp.style.color = dimmedColor;
    return sp
}


linksContainer.append(
    separatorNode()
)
linksContainer.append(
    newSubLink('/submitted?id=' + username, 'submitted')
)
linksContainer.append(
    separatorNode()
)
linksContainer.append(
    newSubLink('/favorites?id=' + username, 'favorites')
)