Greasy Fork is available in English.

D.gg Img Preview Addon

Shows a preview of images linked in destiny.gg

// ==UserScript==
// @name        D.gg Img Preview Addon
// @namespace   D.gg Scripts
// @description Shows a preview of images linked in destiny.gg
// @match       https://www.destiny.gg/embed/chat*
// @connect     discordapp.net
// @connect     discordapp.com
// @connect     pbs.twimg.com
// @connect     polecat.me
// @connect     imgur.com
// @connect     gyazo.com
// @connect     redd.it
// @grant       GM_xmlhttpRequest
// @version     5.0
// @run-at      document-end
// @icon        https://cdn.destiny.gg/2.49.0/emotes/6296cf7e8ccd0.png
// @author      legolas
// ==/UserScript==

const imageRegex = /http.+(redd.it|pbs.twimg.com|(media|cdn).discordapp.(net|com)|imgur.com|gyazo.com|polecat.me).+(png|jpe?g|gifv?)/gm
let overlay;

// START STOLEN FROM VYNEER

class ConfigItem {
  constructor(keyName, defaultValue) {
    this.keyName = keyName;
    this.defaultValue = defaultValue;
  }
}

const configItems = {
  BlurNSFW : new ConfigItem("BlurNSFW",true),
  HideLink : new ConfigItem("HideLink",true)
};

class Config {
  #configItems;
  #configKeyPrefix;
  constructor(configKeys, keyPrefix) {
    this.#configItems = configKeys;
    this.#configKeyPrefix = keyPrefix;
    // Creates setter funcs in this object (config)
    // So when the config.key value is changed it is also saved in localStorage
    for (const key in this.#configItems) {
      const configKey = this.#configItems[key];
      const keyName = configKey.keyName;
      const privateKeyName = `#${keyName}`;
      Object.defineProperty(this, key, {
        set: function (value) {
          // Set the private value
          this[privateKeyName] = value;
          // Save it to persistent storage as well
          this.#save(keyName, value);
        },
        get: function () {
          // Check if value is saved in config object
          if (this[privateKeyName] === undefined) {
            // If not, load it from persistent storage, or use default value
            this[privateKeyName] = this.#load(keyName) ?? configKey.defaultValue;
          }
          return this[privateKeyName];
        },
      });
    }
  }
  #getFullKeyName(configKey) {
    return `${this.#configKeyPrefix}${configKey}`;
  }
  #save(configKey, value) {
    // Persist the value in LocalStorage
    const fullKeyName = this.#getFullKeyName(configKey);
    unsafeWindow.localStorage.setItem(fullKeyName, JSON.stringify(value));
  }
  #load(configKey) {
    // Get the value we persisted, in localStorage
    const fullKeyName = this.#getFullKeyName(configKey);
    const item = unsafeWindow.localStorage.getItem(fullKeyName);
    if (item != null) {
      const parsedItem = JSON.parse(item);
      return parsedItem;
    }
  }
};

const config = new Config(configItems, "img-util.");

let addSettings = () => {

  let settingsArea = document.querySelector("#chat-settings-form");

  // Settings Title
  let settingsTitle = document.createElement("h4");
  settingsTitle.innerHTML = "D.GG Img Preview Settings";

  // Blur Nsfw
  let blurNsfw = document.createElement("div");
  blurNsfw.className = "form-group checkbox";

  let blurNsfwLabel = document.createElement("label");
  blurNsfwLabel.innerHTML = "Blur nsfw/nsfl images";
  blurNsfw.appendChild(blurNsfwLabel);

  let blurNsfwCheck = document.createElement("input");
  blurNsfwCheck.name = "blurNsfw";
  blurNsfwCheck.type = "checkbox";
  blurNsfwCheck.checked = config.BlurNSFW;
  blurNsfwCheck.addEventListener("change", () => config.BlurNSFW = blurNsfwCheck.checked);
  blurNsfwLabel.prepend(blurNsfwCheck);

  // Hide Link
  let hideLink = document.createElement("div");
  hideLink.className = "form-group checkbox";

  let hideLinkLabel = document.createElement("label");
  hideLinkLabel.innerHTML = "Hide Link from previewed messages";
  hideLink.appendChild(hideLinkLabel);

  let hideLinkCheck = document.createElement("input");
  hideLinkCheck.name = "HideLink";
  hideLinkCheck.type = "checkbox";
  hideLinkCheck.checked = config.HideLink;
  hideLinkCheck.addEventListener("change", () => config.HideLink = hideLinkCheck.checked);
  hideLinkLabel.prepend(hideLinkCheck);

  // Add to settings
  settingsArea.appendChild(settingsTitle);
  settingsArea.appendChild(blurNsfw);
  settingsArea.appendChild(hideLink);

  console.log("[D.gg Img Preview] Settings Added")
}

// END STOLEN FROM VYNEER

function waitForElm(selector) { // stolen from stack overflow
    return new Promise(resolve => {
        if (unsafeWindow.document.querySelector(selector)) {
            return resolve(unsafeWindow.document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (unsafeWindow.document.querySelector(selector)) {
                resolve(unsafeWindow.document.querySelector(selector));
                observer.disconnect();
            }
        });
        observer.observe(unsafeWindow.document.body, {
            childList: true,
            subtree: true
        });
    });
}

let chatObserver = new MutationObserver(function(mutations) {
  if(mutations[0].addedNodes[0] && mutations[0].addedNodes[0].tagName == "DIV" && mutations[0].addedNodes[0].querySelector(".externallink")){
    let links = mutations[0].addedNodes[0].querySelectorAll(".externallink ");
    links.forEach((el) => {
      if(imageRegex.test(el.href)){

        GM_xmlhttpRequest({ // get image data since csp blocks us from just using the url directly
          method: "GET",
          url: el.href,
          responseType: 'blob',
          onload: function(res) {

            var reader = new FileReader();
            reader.readAsDataURL(res.response);

            reader.onloadend = function() {

              var base64data = reader.result;
              let PreviewImage = unsafeWindow.document.createElement("img");

              PreviewImage.src = base64data;

              PreviewImage.style.maxHeight = "300px";
              PreviewImage.style.maxWidth = "300px";
              PreviewImage.style.marginLeft = "5px";
              PreviewImage.style.marginBottom = "10px";
              PreviewImage.style.marginTop = "10px";
              PreviewImage.style.display = "block";
              PreviewImage.style.cursor = "pointer";

              let blurred = false;
              if(el.className.includes("nsfw") && config.BlurNSFW || el.className.includes("nsfl") && config.BlurNSFW){
                PreviewImage.style.filter = "blur(15px)";
                blurred = true;
              }

              // PEPE WINS

              PreviewImage.onclick = () => {
                if(blurred){
                  PreviewImage.style.filter = "blur(0px)";
                  blurred = false;
                }else{
                  overlay.style.display = "flex";
                  let PreviewImg = unsafeWindow.document.createElement("img");
                  PreviewImg.src = base64data;
                  PreviewImg.style.maxHeight = "70%";
                  PreviewImg.style.maxWidth = "70%";
                  PreviewImg.style.display = "block";
                  PreviewImg.style.position = "relative";

                  overlay.appendChild(PreviewImg);

                  let openOriginal = document.createElement("a");
                  openOriginal.href = el.href;
                  openOriginal.innerHTML = "Open Original";
                  openOriginal.target = "_blank";
                  openOriginal.style.marginTop = "5px";
                  openOriginal.style.color = "#999";
                  overlay.appendChild(openOriginal)
                }
              }

              el.parentNode.appendChild(PreviewImage);
              if ( config.HideLink ) { el.remove(); };

              unsafeWindow.document.getElementsByClassName("chat-lines")[0].scrollTo(0, unsafeWindow.document.getElementsByClassName("chat-lines")[0].scrollHeight); // Scroll to bottom ( doesnt seem to be working pretty sure its scrolling before img is rendered cba to fix it atm )

            }

          }
        })
      }
    })
  }
});

console.log("[D.gg Img Preview] Connecting")
waitForElm('.chat-lines').then((elm) => {

    overlay  = document.createElement("div");
    overlay.style.position = "absolute";
    overlay.style.justifyContent = "center";
    overlay.style.alignItems = "center";
    overlay.style.inset	= "0px";
    overlay.style.margin = "0px";
    overlay.style.background = "rgba(0,0,0,0.85)";
    overlay.style.zIndex = "999";
    overlay.style.height = "100%";
    overlay.style.width = "100%";
    overlay.style.display = "none";
    overlay.style.flexDirection = "column";

    overlay.onclick = () => {  overlay.style.display = "none"; overlay.innerHTML = ""; };

    document.body.appendChild(overlay)

    chatObserver.observe(unsafeWindow.document.getElementsByClassName("chat-lines")[0], {attributes: false, childList: true, characterData: false, subtree:true});;
    console.log("[D.gg Img Preview] Connected")
    console.log("[D.gg Img Preview] Adding Settings")
    addSettings()
});