真·知乎屏蔽 Truly Zhihu Shield

Shield information you don't want to see.

// ==UserScript==
// @name         真·知乎屏蔽 Truly Zhihu Shield
// @namespace    https://github.com/Virtual-Dimension/TrulyShield/blob/master/TrulyZhihuShield.js
// @version      0.4.0
// @description  Shield information you don't want to see.
// @author       Ciyang
// @license      GPL-3.0
// @match        https://www.zhihu.com/*
// @match        http://www.zhihu.com/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

'use strict';

(() => {
  let getContentElement = () => { return document.getElementById('root'); };
  let isCard = (className) => {
    return className.search('NestComment') !== -1
      || className.search('Card') !== -1
      || className.search('List-item') !== -1
      || className.search('openComment') !== -1;
  };
  let mRequest = (method, href, fn) => {
    let xmlHttp = new window.XMLHttpRequest();
    xmlHttp.open(method, href, true);
    xmlHttp.onreadystatechange = () => { if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { fn(xmlHttp.responseText); } };
    xmlHttp.send();
  };
  let userShieldMap = new Map();
  let excludeEditor = () => { return window.location.href.search("www.zhihu.com/people/") !== -1 || window.location.href.search("www.zhihu.com/org/") !== -1; };
  let getUserShielded = (username, href, fn) => {
    if (!userShieldMap.has(username)) {
      mRequest('get', href, (resText) => {
        let res = (resText.search('已屏蔽') !== -1);
        userShieldMap.set(username, res);
        fn(res);
      });
    } else {
      fn(userShieldMap.get(username));
    }
  };
  let shieldKeywords = [];
  let shieldCount = 0;
  let checkShielded = () => {
    let questionList = document.querySelectorAll('a[data-za-detail-view-element_name]');
    for (const question of questionList) {
      for (const item of shieldKeywords) {
        if (question.innerText.indexOf(item) !== -1) {
          let card = question;
          while (card.id !== 'root' && (!card.className || !isCard(card.className))) { card = card.parentElement; }
          if (card.id !== 'root') {
            card.remove();
            // Remove or Change to shield info
            ++shieldCount;
          }
          break;
        }
      }
    }
    let userList = document.getElementsByClassName('UserLink-link');
    for (const user of userList) {
      if (user.innerText && user.href) {
        let card = user;
        while (card.id !== 'root' && (!card.className || !isCard(card.className))) { card = card.parentElement; }
        if (card.id !== 'root') {
          ((userElement, cardElement) => {
            let url = new window.URL(userElement.href, window.location.href);
            getUserShielded(userElement.innerText, url.href, res => {
              if (res) {
                cardElement.remove();
                // Remove or Change to shield info
                ++shieldCount;
              }
            });
          })(user, card);
        }
      }
    }
  };
  let timer;
  let moCallback = (mutationsList, observer) => {
    for (const mutation of mutationsList) {
      if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes.length) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(checkShielded, 50);
      }
    }
  };
  let observerConfig = { childList: true, subtree: true };
  let openFlag = false;
  let checkFullPageModal = () => {
    if (!openFlag && document.getElementsByClassName('Modal--fullPage').length) {
      openFlag = true;
      let modal = document.getElementsByClassName('Modal--fullPage')[0];
      let observer = new window.MutationObserver(moCallback);
      observer.observe(modal, observerConfig);
      if (timer) clearTimeout(timer);
      timer = setTimeout(checkShielded, 50);
    }
    if (!document.getElementsByClassName('Modal--fullPage').length) { openFlag = false; }
  };
  let downloadObject = (filename, object) => {
    let downloadElement = document.createElement('a');
    downloadElement.href = ['data:application/json;', window.JSON.stringify(object)];
    downloadElement.download = filename;
    downloadElement.click();
  };
  let addShieldMenu = () => {
    let shieldMenu = document.createElement('div');
    shieldMenu.style.border = 'solid 1px #0000001c';
    shieldMenu.style.position = 'fixed';
    shieldMenu.style.top = '10%';
    shieldMenu.style.left = '0.5%';
    shieldMenu.style.maxWidth = '40px';
    shieldMenu.style.textAlign = "center";
    let addButton = document.createElement('button');
    addButton.innerHTML = '<svg t=\"1573460096262\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1080\" width=\"32\" height=\"32\"><path d=\"M874.0352 149.956267C777.335467 53.256533 648.7552 0 512 0 375.236267 0 246.664533 53.248 149.956267 149.956267 53.256533 246.664533 0 375.236267 0 512s53.248 265.335467 149.956267 362.043733C246.664533 970.743467 375.236267 1024 512 1024s265.335467-53.248 362.043733-149.956267C970.743467 777.335467 1024 648.763733 1024 512s-53.256533-265.335467-149.9648-362.043733zM554.666667 469.333333V315.682133A42.606933 42.606933 0 0 0 512 273.066667a42.666667 42.666667 0 0 0-42.666667 42.615466V469.333333H315.682133A42.606933 42.606933 0 0 0 273.066667 512a42.666667 42.666667 0 0 0 42.615466 42.666667H469.333333v153.6512A42.606933 42.606933 0 0 0 512 750.933333a42.666667 42.666667 0 0 0 42.666667-42.615466V554.666667h153.6512A42.606933 42.606933 0 0 0 750.933333 512a42.666667 42.666667 0 0 0-42.615466-42.666667H554.666667z m259.029333 344.362667C733.115733 894.293333 625.962667 938.666667 512 938.666667c-113.962667 0-221.115733-44.381867-301.696-124.970667C129.706667 733.1072 85.333333 625.962667 85.333333 512c0-113.962667 44.381867-221.115733 124.970667-301.696C290.8928 129.706667 398.037333 85.333333 512 85.333333c113.962667 0 221.115733 44.381867 301.696 124.970667C894.293333 290.884267 938.666667 398.037333 938.666667 512c0 113.9712-44.381867 221.115733-124.970667 301.696z\" fill=\"#707070\" p-id=\"1081\"></path></svg>';
    addButton.style.margin = '1px';
    addButton.onclick = async () => {
      let res = window.prompt('输入想添加的屏蔽关键词,添加单个不需要输入引号。如果想添加多个请用\"[]\"包括并使用\",\"分割。可输入一个外链直接导入词库。', '[\"XXX\",\"YYY\"]');
      if (res === null || res === '') { return; }
      let cnt = 0;
      try {
        let url = new window.URL(res);
        res = await new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: "GET",
            headers: { "Accept": "application/json" },
            url: url.href,
            onload: (response) => { resolve(response.responseText); }
          });
        });
      } catch (err) { }
      try {
        res = window.JSON.parse(res);
        for (const str of res) {
          if (shieldKeywords.indexOf(str) === -1) {
            shieldKeywords.push(str);
            ++cnt;
          }
        }
      } catch (err) {
        if (shieldKeywords.indexOf(res) === -1) {
          shieldKeywords.push(res);
          ++cnt;
        }
      }
      if (cnt) { window.localStorage.setItem('TrulyShield', window.JSON.stringify(shieldKeywords)); }
      window.alert(['成功添加', cnt, "个屏蔽关键字"].join(''));
    };
    let removeButton = document.createElement('button');
    removeButton.innerHTML = '<svg t="1573478908483" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1066" width="32" height="32"><path d="M874.0352 149.956267C777.335467 53.256533 648.7552 0 512 0 375.236267 0 246.664533 53.248 149.956267 149.956267 53.256533 246.664533 0 375.236267 0 512s53.248 265.335467 149.956267 362.043733C246.664533 970.743467 375.236267 1024 512 1024s265.335467-53.248 362.043733-149.956267C970.743467 777.335467 1024 648.763733 1024 512s-53.256533-265.335467-149.9648-362.043733z m-60.330667 663.739733C733.1072 894.293333 625.962667 938.666667 512 938.666667c-113.962667 0-221.115733-44.381867-301.696-124.970667C129.706667 733.1072 85.333333 625.962667 85.333333 512c0-113.962667 44.381867-221.115733 124.970667-301.696C290.8928 129.706667 398.037333 85.333333 512 85.333333c113.962667 0 221.115733 44.381867 301.696 124.970667C894.293333 290.884267 938.666667 398.037333 938.666667 512c0 113.9712-44.381867 221.115733-124.970667 301.696zM273.066667 512c0-23.560533 19.029333-42.666667 42.615466-42.666667h392.635734A42.666667 42.666667 0 0 1 750.933333 512c0 23.560533-19.029333 42.666667-42.615466 42.666667H315.682133A42.666667 42.666667 0 0 1 273.066667 512z" fill="#707070" p-id="1067"></path></svg>'
    removeButton.style.margin = '1px';
    removeButton.onclick = () => {
      let res = window.prompt('输入想移除的屏蔽关键词,使用/all清空屏蔽关键词。');
      if (res === null || res === '') { return; }
      let res2 = res.toLowerCase();
      let cnt = 0;
      if (res2 === '/all') {
        cnt += shieldKeywords.length;
        shieldKeywords = [];
      } else {
        res2 = shieldKeywords.indexOf(res);
        if (res2 !== -1) {
          shieldKeywords.slice(res2, 1);
          ++cnt;
        }
      }
      if (cnt) { window.localStorage.setItem('TrulyShield', window.JSON.stringify(shieldKeywords)); }
      window.alert(['成功移除', cnt, "个屏蔽关键字"].join(''));
    };
    let downloadButton = document.createElement('button');
    downloadButton.innerHTML = '<svg t=\"1573465392430\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1579\" width=\"32\" height=\"32\"><path d=\"M725.333333 0H196.266667C111.786667 0 42.666667 69.12 42.666667 153.6v716.8c0 84.48 69.12 153.6 153.6 153.6h631.466666c84.48 0 153.6-69.12 153.6-153.6V256L725.333333 0z m170.666667 870.4c0 37.546667-30.72 68.266667-68.266667 68.266667H196.266667c-37.546667 0-68.266667-30.72-68.266667-68.266667V153.6c0-37.546667 30.72-68.266667 68.266667-68.266667h409.6v221.866667c0 37.546667 30.72 68.266667 68.266666 68.266667h221.866667v494.933333zM708.266667 290.133333c-9.386667 0-17.066667-7.68-17.066667-17.066666V85.333333l204.8 204.8H708.266667z\" fill=\"#4990EE\" p-id=\"1580\"></path><path d=\"M588.8 716.8H298.666667c-23.893333 0-42.666667 18.773333-42.666667 42.666667s18.773333 42.666667 42.666667 42.666666h290.133333c23.893333 0 42.666667-18.773333 42.666667-42.666666s-18.773333-42.666667-42.666667-42.666667zM256 571.733333c0 23.893333 18.773333 42.666667 42.666667 42.666667h426.666666c23.893333 0 42.666667-18.773333 42.666667-42.666667s-18.773333-42.666667-42.666667-42.666666H298.666667c-23.893333 0-42.666667 18.773333-42.666667 42.666666z\" fill=\"#707070\" p-id=\"1715\"></path></svg>'
    downloadButton.style.margin = '1px';
    downloadButton.onclick = () => { downloadObject("data.json", shieldKeywords); };
    let shieldCountElement = document.createElement('button');
    shieldCountElement.style.border = 'solid 1px #3498db';
    shieldCountElement.style.borderRadius = '50%';
    shieldCountElement.style.margin = '1px';
    shieldCountElement.style.overflow = 'hidden';
    shieldCountElement.style.width = '32px';
    shieldCountElement.style.height = '32px';
    setInterval(() => { shieldCountElement.innerText = shieldCount }, 500);
    shieldMenu.appendChild(addButton);
    shieldMenu.appendChild(removeButton);
    shieldMenu.appendChild(downloadButton);
    shieldMenu.appendChild(shieldCountElement);
    document.body.appendChild(shieldMenu);
  };
  let initShieldKeywords = () => {
    let res = [];
    try {
      let data = window.localStorage.getItem('TrulyShield');
      if (data && data.length) { res = window.JSON.parse(data); }
    } catch (err) {
      window.localStorage.setItem('TrulyShield', window.JSON.stringify(res));
    }
    shieldKeywords = res;
  };
  window.addEventListener('load', (event) => {
    initShieldKeywords();
    addShieldMenu();
    if (excludeEditor()) { userShieldMap.set(document.getElementsByClassName('ProfileHeader-name')[0].innerText, false); }
    if (timer) clearTimeout(timer);
    timer = setTimeout(checkShielded, 50);
    let targetNode = getContentElement();
    let observer = new window.MutationObserver(moCallback);
    observer.observe(targetNode, observerConfig);
    setInterval(checkFullPageModal, 10);
  });

})();