kbin enhancement script

Few small changes to the kbin UI while they still develop some features

// ==UserScript==
// @name     kbin enhancement script
// @description Few small changes to the kbin UI while they still develop some features
// @namespace com.sirpsychomantis
// @license MIT
// @version  1.11
// @grant    none
// @run-at document-end
// @match  https://fedia.io/*
// @match  https://kbin.social/*
// ==/UserScript==


(function(){
  const version = "1.9";
  
  const style = document.createElement('style');
  style.textContent = `
    .entry figure {overflow: hidden}
    .comment .badge {padding:.25rem;margin-left:5px}
    #kes-version-dialog {position: fixed; width: 100vw; height: 100vh; top: 0; left: 0; display: flex; align-items: center; justify-content: center; background-color: rgba(0,0,0,.3); z-index: 9999999}
    .kes-version-dialog-content {background: #ddd; color: #444; position: relative; padding: 40px}
		.kes-expand {grid-gap: 0; padding-left:55px}
    .kes-blur {filter: blur(4px); transition-duration: 0.5s}
		.kes-blur-large {filter: blur(15px)}
    .kes-blur:hover {filter: none}
  `;
  document.head.appendChild(style);
  
  
  const allSettings = [
    {name: "Show domains", value:"show-domains"},
    {name: "Show collapse comment", value:"show-collapse"},
    {name: "Show collapse replies", value:"show-collapse-replies"},
    {name: "Replies start collapsed", value:"start-collapse-replies", default: "false"},
    {name: "Move comment box to top", value:"comment-box-top"},
    {name: "Reply box improvements", value:"comment-cancel"},
    {name: "Hide known NSFW domains", value:"nsfw-hide"},
    {name: "Blur known NSFW domains", value:"nsfw-blur"},
    {name: "Hide random sidebar posts", value:"hide-random"},
    {name: "Add OP tag", value:"op-tag"}
  ];
  
  allSettings.forEach(setting => {
    if (setting.default === "false" && localStorage.getItem("setting-" + setting.value) === null) {
      localStorage.setItem("setting-" + setting.value, "false");
    }
  });
  
  function getSetting(setting) {
    let value = localStorage.getItem("setting-" + setting);
    if (value === null)
      value = "true";
    return value === "true";
  }
  function setSetting(setting, value) {
    localStorage.setItem("setting-" + setting, value);
    location.reload();
  }
  
  
  function addDomain(link) {
    const parts = link.title.split("@");
    if (parts[2] !== location.hostname && !link.innerText.includes("@" + parts[2])) {
      const linkText = link.childNodes[link.childNodes.length-1];
      linkText.nodeValue += "@" + parts[2];
    }
  }
  function addDomains() {
    document.querySelectorAll(".magazine-inline, .user-inline").forEach(link => {
      addDomain(link);
    });
    
    
    const config = { childList: true };
    const callback = (mutationList, observer) => {
      for (const mutation of mutationList) {
        mutation.addedNodes.forEach(container => {
        	container.querySelectorAll(".magazine-inline, .user-inline").forEach(link => {
            addDomain(link);
          });
        });
      }
    };

    const observer = new MutationObserver(callback);
    const content = document.querySelector("div#content > div");
    if (content)
    	observer.observe(content, config);
  }
  
  
  function getComments(comment, allComments) {
    const id = comment.id.split('-')[2];
    
    allComments.push(comment);
    const subComments = comment.parentElement.querySelectorAll('blockquote[data-subject-parent-value="'+id+'"]');
    subComments.forEach(blockquote => { getComments(blockquote, allComments); });
  }
  function getCollapsos(comment, allCollapsos) {
    const id = comment.id.split('-')[2];
    
    if (comment.classList.contains('kes-expand'))
    	allCollapsos.push(comment);
    
    const subComments = comment.parentElement.querySelectorAll('blockquote[data-subject-parent-value="'+id+'"]');
    subComments.forEach(blockquote => { getCollapsos(blockquote, allCollapsos); });
  }
  function removeAllCollapsos(blockquote) {
    // Just remove all these for now, don't want to figure out how to do this cleanly right now.
    const allCollapsos = [];
    getCollapsos(blockquote, allCollapsos);
    allCollapsos.forEach(comment => { comment.remove() });
  }
  function expandComment(blockquote) {
    const allComments = [];
    getComments(blockquote, allComments);
    allComments.forEach(comment => { comment.style.display="" });
    
    removeAllCollapsos(blockquote);
  }
  function collapseComment(blockquote) {
    const id = blockquote.id.split('-')[2];
    let commentLevel = "1";
    blockquote.classList.forEach(classItem => {
    	if (classItem.includes("comment-level"))
        commentLevel = classItem.split("--")[1];
    });
    
    const allComments = [];
    getComments(blockquote, allComments);
    allComments.forEach(comment => { comment.style.display="none" });

    const username = blockquote.querySelector("header a").innerText;
    const time = blockquote.querySelector("header time").innerText;

    const newBlockquote = document.createElement('blockquote');
    newBlockquote.className = 'kes-expand section comment entry-comment comment-level--' + commentLevel;
    newBlockquote.dataset.subjectParentValue = id;
    newBlockquote.innerHTML = '<header><a href="javascript:;">' + username + ', ' + time + ' [+]</a></header>';
    newBlockquote.querySelector('a').addEventListener("click", () => {expandComment(blockquote)});
    blockquote.parentNode.insertBefore(newBlockquote, blockquote);
  }
  function getTotalReplyCount(blockquote) {
    const allComments = [];
    getComments(blockquote, allComments);
    return allComments.length - 1;
  }
  
  function addCollapseLinks() {
    if (location.pathname.startsWith('/m')) {
      const comments = document.querySelectorAll("blockquote.comment");
      comments.forEach(blockquote => {
      	const menu = blockquote.querySelector("header");
        if (!menu.innerText.includes('[-]')) {
          const newA = document.createElement('a');
          newA.href = "javascript:;";
          newA.className = "kes-collapse";
          newA.innerHTML = '[-]';
          menu.appendChild(newA);
        }
      });
      
      document.querySelectorAll(".kes-collapse").forEach(link => {link.addEventListener("click", () => {
        const blockquote = link.closest("blockquote.comment");
        collapseComment(blockquote);
      })});
    }
  }
  
  function getAllReplies(blockquote) {
    const allComments = [];
    getComments(blockquote, allComments);
    allComments.splice(allComments.indexOf(blockquote), 1);
    return allComments;
  }
  
  
  function toggleReplies(blockquote, display) {
    const id = blockquote.id.split('-')[2];
    const allReplies = getAllReplies(blockquote);
    
    let anyHidden = false;
    allReplies.forEach(reply => { 
      if (reply.style.display == 'none')
        anyHidden = true;
    });
    
    allReplies.forEach(comment => { comment.style.display = anyHidden ? '' : 'none' });
    
    removeAllCollapsos(blockquote);
  }
  
  function addCollapseRepliesLinks() {
    if (location.pathname.startsWith('/m')) {
      const comments = document.querySelectorAll("blockquote.comment-level--1");
      comments.forEach(blockquote => {
        const id = blockquote.id.split('-')[2];
        const subComments = blockquote.parentElement.querySelectorAll('blockquote[data-subject-parent-value="'+id+'"]');
        
        if (subComments.length > 0) {
          const menu = blockquote.querySelector("footer menu");
          const newLi = document.createElement('li');
          newLi.innerHTML = '<a href="javascript:;" class="kes-collapse-replies">toggle replies ('+getTotalReplyCount(blockquote)+')</a>';
          menu.appendChild(newLi);
        }
      });
      
      document.querySelectorAll(".kes-collapse-replies").forEach(link => {link.addEventListener("click", () => {
        const blockquote = link.closest("blockquote.comment");
        
        toggleReplies(blockquote);
      })});
    }
  }
  function collapseAllReplies() {
    const comments = document.querySelectorAll("blockquote.comment-level--2");
    comments.forEach(blockquote => {
      collapseComment(blockquote);
    });
  }
  
  
  function moveCommentBox() {
    const commentAdd = document.querySelector('#comment-add');
    if (commentAdd)
    	commentAdd.parentNode.insertBefore(commentAdd, document.querySelector('#comments'));
  }
  
  
  function removeReplyBox(container) {
    container.innerHTML = '';
    container.style = '';
  }
  function addCommentCancelButton(container) {
    const list = container.querySelector('div.actions ul');
    
    const newLi = document.createElement('li');
    newLi.innerHTML = '<div><button class="btn btn__primary">Cancel</button></div>';
    list.appendChild(newLi);
    newLi.querySelector('button').addEventListener("click", () => { removeReplyBox(container) });
  }
  function fixMarkdownButtons(form) {
    const formActionSplit = form.action.split('/');
    const newId = 'entry_comment_body_' + formActionSplit[formActionSplit.length-1];
    
    form.querySelector('#entry_comment_body').id = newId;
    form.querySelector('markdown-toolbar').setAttribute('for', newId);
  }
  function observeReplyAdded() {
    const config = { childList: true };
    const callback = (mutationList, observer) => {
      for (const mutation of mutationList) {
        const container = mutation.target;
        const form = container.querySelector('form.comment-add');
        
        if (form !== null) {
          fixMarkdownButtons(form);
          addCommentCancelButton(container);
        }
      }
    };

    const observer = new MutationObserver(callback);
    document.querySelectorAll('blockquote.comment footer div.js-container').forEach(container => { observer.observe(container, config) })
  }
  
  
  // Thanks u/le__el
  function addBlur(container) {
    container.querySelectorAll("img").forEach(el => {
        el.classList.add('kes-blur');
    });
    container.querySelectorAll("figure img").forEach(el => {
        el.classList.add('kes-blur-large');
    });
  }
  const nsfwDomains = [
    "lemmynsfw.com",
    "redgifs.com"
  ];
  function hideNSFW() {
    document.querySelectorAll("article").forEach(article => {
      const magazineInline = article.querySelector(".magazine-inline");
      const entryDomain = article.querySelector(".entry__domain a");
      if (article.querySelector("small.danger") !== null
         || (entryDomain && nsfwDomains.includes(entryDomain.innerText))
         || ( magazineInline && nsfwDomains.includes(magazineInline.title.split('@')[2]) )
         ) {
        if (getSetting("nsfw-hide")) {
      		article.remove();
        } else {
          addBlur(article);
        }
      }
    });
  }
  
  function hideRandom() {
    const posts = document.querySelector('#sidebar section.posts');
    if (posts)
      posts.style = "display:none;";
    
    const entries = document.querySelector('#sidebar section.entries');
    if (entries)
      entries.style = "display:none;";
  }
  
  function addOPTag() {
    document.querySelectorAll('blockquote.author > header').forEach(header => {
    	const opTag = document.createElement('small');
      opTag.className = "badge kbin-bg";
      opTag.innerText = "OP";
      header.appendChild(opTag);
    });
  }
  
  
  function generateSettingDiv(settingDisplay, setting) {
    const settingValue = getSetting(setting);
    const newDiv = document.createElement('div');
    newDiv.className = "row";
    newDiv.innerHTML = `<span>${settingDisplay}:</span>
      <div>
        <a class="kes-setting-yes link-muted ${settingValue ? 'active' : ''}" href="javascript:;" data-setting="${setting}">
          Yes
        </a>
        |
        <a class="kes-setting-no link-muted ${settingValue ? '' : 'active'}" href="javascript:;" data-setting="${setting}">
          No
        </a>
      </div>`;
    
    return newDiv;
  }
  function addHTMLSettings() {
    const settingsList = document.querySelector(".settings-list");
    
    const header = document.createElement('strong');
    header.textContent = "kbin enhancement script";
    settingsList.appendChild(header);
    
    allSettings.forEach(setting => { settingsList.appendChild(generateSettingDiv(setting.name, setting.value)) });
                             
    document.querySelectorAll(".kes-setting-yes").forEach(link => { link.addEventListener("click", () => {setSetting(link.dataset.setting, true) })});
    document.querySelectorAll(".kes-setting-no").forEach(link => { link.addEventListener("click", () => {setSetting(link.dataset.setting, false) })});
  }
  
  addHTMLSettings();
  if (getSetting("show-domains"))
    addDomains();
  if (getSetting("show-collapse"))
    addCollapseLinks();
  if (getSetting("show-collapse-replies"))
    addCollapseRepliesLinks();
  if (getSetting("start-collapse-replies"))
    collapseAllReplies();
  if (getSetting("comment-box-top"))
    moveCommentBox();
  if (getSetting("comment-cancel"))
    observeReplyAdded();
  if (getSetting("nsfw-blur") || getSetting("nsfw-hide"))
    hideNSFW();
  if (getSetting("hide-random"))
    hideRandom();
  if (getSetting("op-tag"))
    addOPTag();
  
  
  
  
  if (localStorage.getItem("setting-changelog-version") != version) {
    const message = `<strong>kbin enhancement script version: ${version}</strong><br>
			Thanks for downloading! You can always toggle on and off features in the kbin sidebar settings.<br>Recent changes:
			<ul>
        <li>OP tag in comments</li>
        <li>Hide random sidebar</li>
				<li>Fixed infinite scroll not showing domains</li>
        <li>Additional NSFW protection</li>
				<li>Fixed markdown buttons and added "Cancel" when replying</li>
        <li>Bug Fixes</li>
			</ul>
		`
    
    
    const versionDiv = document.createElement('div');
    versionDiv.id = 'kes-version-dialog';
    versionDiv.innerHTML = '<div class="kes-version-dialog-content">'+message+'<br><button>Close</button></div>';
    document.body.appendChild(versionDiv);
    
    document.querySelector('#kes-version-dialog button').addEventListener("click", () => {
      document.querySelector('#kes-version-dialog').remove();
      localStorage.setItem("setting-changelog-version", version);
    });
  }
})();