EfogDev / Reddikabu

// ==UserScript==
// @name         Reddikabu
// @namespace    https://reddit.com/
// @version      1.0
// @author       Efog
// @match        https://www.reddit.com/*
// @match        https://reddit.com/*
// @match        http://www.reddit.com/*
// @match        http://reddit.com/*
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  class Reddikabu {
    constructor() {
      this.auth = null;
      this.visibleCardTime = 0;

      this.applyPushStateHook();
      this.applyKeyboardHook();
      this.applyXHRHook();
      this.applyScrollHook();
      this.convertLinks();

      Reddikabu.log('Initialization successfull!');
    }

    applyScrollHook() {
      const SCROLL_THRESHOLD = 100;
      let timeoutId = null;

      setTimeout(() => {
        this.handleScroll();
      });

      window.addEventListener('scroll', () => {
        clearTimeout(timeoutId);

        timeoutId = setTimeout(() => {
          this.handleScroll();
        }, SCROLL_THRESHOLD);
      });
    }

    applyXHRHook() {
      const _open = XMLHttpRequest.prototype.open;
      const _setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
      const _this = this;

      XMLHttpRequest.prototype.setRequestHeader = function (...args) {
        const [name, content] = args;

        if (name === 'Authorization' && content) {
          _this.auth = content;
        }

        return _setRequestHeader.apply(this, args);
      };

      XMLHttpRequest.prototype.open = function (...args) {
        this.addEventListener('load', () => {
          _this.refresh(...args);
        });

        return _open.apply(this, args);
      };
    }

    applyPushStateHook() {
      const _pushState = history.pushState;
      const _this = this;
      
      history.pushState = (function (...args) {
        _this.refresh(...args);

        return _pushState.apply(this, args);
      });
    }

    applyKeyboardHook() {
      document.body.addEventListener('keydown', event => {
        if (Reddikabu.isInputElement(event.target))
          return;

        if (this.makeAction(event.key, event)) {
          event.preventDefault();
          event.stopPropagation();
          return false;
        }
      });
    }

    handleScroll() {
      const HIDE_THRESHOLD = 400;
      const currentCard = Reddikabu.getTopVisibleCard();

      Reddikabu.logSeparator();
      Reddikabu.log('Current card:', currentCard);
      Reddikabu.log('Previous card:', this.visibleCard);
      Reddikabu.log('Time between:', Date.now() - this.visibleCardTime);

      if (this.visibleCard && (Date.now() - this.visibleCardTime > HIDE_THRESHOLD) && currentCard !== this.visibleCard) {
        try {
          Reddikabu.log(`Hiding post ID#${this.visibleCard.id}.`);

          this.hidePost(this.visibleCard.id);
        } catch (e) {
          Reddikabu.log('Hiding error!', e);
        }
      }

      this.visibleCard = currentCard;
      this.visibleCardTime = Date.now();
    }

    hidePost(postId) {
      if (!this.auth)
        return;

      const xhr = new XMLHttpRequest();
      xhr.open('POST', 'https://oauth.reddit.com/api/hide?redditWebClient=web2x&app=web2x-client-production&raw_json=1&gilding_detail=1');
      xhr.setRequestHeader('Authorization', this.auth);
      xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
      xhr.send(`id=${postId}`);
    }

    convertLinks() {
      Array.from(document.querySelectorAll('[data-test-id=comment] p > a:first-of-type'))
        .forEach(link => {
          try {
            const [, extension] = link.href.match(/^.*\.([a-z0-9]+)$/);

            const isHttps = link.href.startsWith('https://');
            const isYoutube = /^https:\/\/www\.youtube\.com\/watch\?v\=([a-z0-9]+?)$/.test(link.href);
            const isImage = extension === 'jpg' || extension === 'jpeg' || extension === 'png' || extension === 'gif';

            if (!isHttps)
              return;

            if (isImage) {
              Reddikabu.replaceLinkByImage(link);
            } else if (isYoutube) {
              Reddikabu.replaceLinkByYoutube(link);
            }
          } catch (e) {}
        });
    }

    static replaceLinkByImage(link) {
      const image = document.createElement('img');
      image.setAttribute('src', link);
      image.setAttribute('style', `
        max-width: 60%;
        display: block;
      `);

      link.outerHTML = image.outerHTML;
    }

    static replaceLinkByYoutube(link) {
      try {
        const iFrame = document.createElement('iframe');
        iFrame.setAttribute('src', `https://www.youtube.com/embed/${link.href.split('?v=')[1]}`);
        iFrame.setAttribute('width', '560');
        iFrame.setAttribute('height', '315');
        iFrame.setAttribute('frameborder', '0');
        iFrame.setAttribute('allow', 'encrypted-media');
        iFrame.setAttribute('allowfullcreen', '');

        link.outerHTML = iFrame.outerHTML;
      } catch (e) {}
    }

    makeAction(key, event) {
      if ((key === 'c' || key === Reddikabu.getRussianCharacter('c')) && event.ctrlKey)
        return false;

      const SCROLL_FOR = 200;
      const currentCard = Reddikabu.getTopVisibleCard();

      const cardActionCharacters = ['w', 'a', 's', 'd', 'e'];
      const russianCardActionCharacters = cardActionCharacters.map(Reddikabu.getRussianCharacter);

      const allCardActionsCharacters = [...cardActionCharacters, ...russianCardActionCharacters];

      if (allCardActionsCharacters.includes(key) && !currentCard)
        return false;

      switch (key) {
        case 'w':
        case Reddikabu.getRussianCharacter('w'):
          currentCard.querySelector('[data-click-id=upvote]').click();
          break;
        case 's':
        case Reddikabu.getRussianCharacter('s'):
          currentCard.querySelector('[data-click-id=downvote]').click();
          break;
        case 'a': 
        case Reddikabu.getRussianCharacter('a'):
          Reddikabu.scrollToCard(Reddikabu.getCardSibling(currentCard, -1));
          break;
        case 'd': 
        case Reddikabu.getRussianCharacter('d'):
          Reddikabu.scrollToCard(Reddikabu.getCardSibling(currentCard));
          break;
        case 'e': 
        case Reddikabu.getRussianCharacter('e'):
          Reddikabu.openPost(currentCard);
          break;
        case '`':
        case Reddikabu.getRussianCharacter('`'):
          window.close();
          break;
        case 'z':
        case Reddikabu.getRussianCharacter('z'):
          Reddikabu.setScrollTop(Reddikabu.getScrollTop() - SCROLL_FOR);
          break;
        case 'c':
        case Reddikabu.getRussianCharacter('c'):
          Reddikabu.setScrollTop(Reddikabu.getScrollTop() + SCROLL_FOR);
          break;
        default:
          return false;
      }

      return true;
    }

    static getRussianCharacter(character) {
      const mapping = {
        'w': 'ц',
        's': 'ы',
        'a': 'ф',
        'd': 'в',
        'e': 'у',
        '`': 'ё',
        'z': 'я',
        'c': 'с',
      };

      return mapping[character] || null;
    }

    static openPost(card) {
      const link = card.querySelector('a[data-click-id=body]');
      const url = link.href;

      window.open(url, '_blank');
    }

    static getScrollTop() {
      return document.scrollingElement.scrollTop;
    }

    static setScrollTop(y) {
      window.scroll({top: y, behavior: 'smooth' });
    }

    static scrollToCard(card) {
      const magicMargin = 10;

      if (!card)
        return;

      Reddikabu.setScrollTop(Reddikabu.getScrollTop() + card.getClientRects()[0].y - Reddikabu.getHeader().clientHeight - magicMargin);
    }

    static getTopVisibleCard() {
      return Array.from(document.querySelectorAll('.Post.scrollerItem'))
        .find(post => {
          const rect = post.getClientRects();

          if (!rect || !rect[0])
            return;

          return rect[0].y > Reddikabu.getHeader().clientHeight;
        });
    }

    static getCardSibling(card, direction = 1) {
      const fn = direction === 1 ? 'nextSibling' : 'previousSibling';
      const sibling = card.parentNode.parentNode[fn];

      if (!sibling)
        return null;

      const isBlank = sibling.querySelector('.scrollerItem.Blank');

      if (isBlank) {
        return Reddikabu.getCardSibling(isBlank, direction);
      } else {
        return sibling.querySelector('.Post.scrollerItem');
      }
    }

    static getHeader() {
      return document.querySelector('header');
    }

    static isInputElement(element) {
      return element.isContentEditable || element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea';
    }

    refresh(...args) {
      setTimeout(() => {
        this.convertLinks();
      });
    }

    static logSeparator() {
      console.log('\n');
    }

    static log(...messages) {
      const [message, ...other] = messages;

      console.log(`%c[Reddikabu] %c[${new Date().toLocaleTimeString()}] %c${message}`, 'color: red;', 'color: blue;', 'color: gray;', ...other);
    }
  }

  const reddikabu = new Reddikabu();  
})();