DGG Auto Image Embed (Release 09/07/25 - dedupe)

Shows a preview of images linked in destiny.gg chat without opening new tabs. NSFW blur toggle + preserves autoscroll + optional 4chan block. Adds robust de-duplication.

As of 2025-09-07. See the latest version.

// ==UserScript==
// @name        DGG Auto Image Embed (Release 09/07/25 - dedupe)
// @namespace   boofus
// @description Shows a preview of images linked in destiny.gg chat without opening new tabs. NSFW blur toggle + preserves autoscroll + optional 4chan block. Adds robust de-duplication.
// @match       https://www.destiny.gg/embed/chat*
// @icon        https://cdn.destiny.gg/2.49.0/emotes/6296cf7e8ccd0.png
// @version     10.2.2
// @author      boofus (credits: legolas, vyneer)
// @license     MIT
// @grant       GM_xmlhttpRequest
// @run-at      document-end
// @connect     discordapp.net
// @connect     discordapp.com
// @connect     pbs.twimg.com
// @connect     polecat.me
// @connect     imgur.com
// @connect     i.imgur.com
// @connect     gyazo.com
// @connect     redd.it
// @connect     i.redd.it
// @connect     catbox.moe
// @connect     i.4cdn.org
// ==/UserScript==

const imageRegex = /https?:\/\/(?:i\.redd\.it|redd\.it|pbs\.twimg\.com|(?:media|cdn)\.discordapp\.(?:net|com)|i\.imgur\.com|imgur\.com|gyazo\.com|polecat\.me|catbox\.moe|i\.4cdn\.org)\/[^\s"'<>]+?\.(?:png|jpe?g|gif|gifv|webp)(?:\?[^\s"'<>]*)?$/i;

let overlay;

// ---------------- settings ----------------
class ConfigItem { constructor(keyName, defaultValue){ this.keyName=keyName; this.defaultValue=defaultValue; } }
const configItems = {
  BlurNSFW : new ConfigItem("BlurNSFW", true),
  HideLink : new ConfigItem("HideLink", true),
  Block4cdn: new ConfigItem("Block4cdn", true),
};
class Config {
  #items; #prefix;
  constructor(items, prefix){ this.#items=items; this.#prefix=prefix;
    for (const key in items){
      const {keyName, defaultValue} = items[key];
      const privateKey = `#${keyName}`;
      Object.defineProperty(this, key, {
        set(v){ this[privateKey]=v; unsafeWindow.localStorage.setItem(`${prefix}${keyName}`, JSON.stringify(v)); },
        get(){ if (this[privateKey]===undefined){ const raw=unsafeWindow.localStorage.getItem(`${prefix}${keyName}`); this[privateKey]=raw!=null?JSON.parse(raw):defaultValue; } return this[privateKey]; }
      });
    }
  }
}
const config = new Config(configItems, "img-util.");
let shouldStickToBottom = true;

function getChatEl(){ return unsafeWindow.document.getElementsByClassName("chat-lines")[0]; }
function isAtBottom(el, threshold=2){ return (el.scrollHeight - el.scrollTop - el.clientHeight) <= threshold; }
function scrollToBottom(el){ requestAnimationFrame(()=>{ el.scrollTop = el.scrollHeight; }); }

function addSettings(){
  const settingsArea = document.querySelector("#chat-settings-form");
  if (!settingsArea) { console.warn("[DGG Img Preview] settings form not found"); return; }
  const title = document.createElement("h4"); title.textContent = "D.GG Img Preview Settings";
  const addCheckbox = (labelText, keyName) => {
    const wrap=document.createElement("div"); wrap.className="form-group checkbox";
    const label=document.createElement("label"); label.textContent=labelText;
    const input=document.createElement("input"); input.type="checkbox"; input.name=keyName; input.checked=!!config[keyName];
    input.addEventListener("change", ()=>{ config[keyName]=input.checked; });
    label.prepend(input); wrap.appendChild(label); return wrap;
  };
  settingsArea.append(title,
    addCheckbox("Blur nsfw/nsfl images", "BlurNSFW"),
    addCheckbox("Hide link after preview", "HideLink"),
    addCheckbox("Disable 4chan (i.4cdn.org) auto-embeds", "Block4cdn"));
  console.log("[DGG Img Preview] Settings Added");
}

// ---------------- utilities ----------------
function waitForElm(selector){
  return new Promise(resolve=>{
    const found=unsafeWindow.document.querySelector(selector);
    if (found) return resolve(found);
    const obs=new MutationObserver(()=>{
      const el=unsafeWindow.document.querySelector(selector);
      if (el){ obs.disconnect(); resolve(el); }
    });
    obs.observe(unsafeWindow.document.body, { childList:true, subtree:true });
  });
}

// ---------------- core logic ----------------
// In-flight guard to avoid parallel dupes on the same element.
const inFlight = new WeakSet();

function handleLink(el){
  // Already handled or being handled?
  if (el.dataset.dggPreviewed === "1" || inFlight.has(el)) return;

  // Optional domain block
  if (config.Block4cdn && /https?:\/\/i\.4cdn\.org\//i.test(el.href)) return;

  // Must look like a direct image
  if (!imageRegex.test(el.href)) return;

  // Mark immediately to prevent re-processing on subsequent mutations
  el.dataset.dggPreviewed = "1";
  inFlight.add(el);

  GM_xmlhttpRequest({
    method: "GET",
    url: el.href,
    responseType: "blob",
    onload: function(res){
      const reader = new FileReader();
      reader.readAsDataURL(res.response);
      reader.onloadend = function(){
        const base64 = reader.result;

        // If a previous preview somehow exists (e.g., remount), skip adding another
        const parent = el.parentNode;
        if (parent && parent.querySelector && parent.querySelector(`img[data-dgg-preview-src="${CSS.escape(el.href)}"]`)){
          if (config.HideLink) el.remove();
          inFlight.delete(el);
          return;
        }

        const img = unsafeWindow.document.createElement("img");
        img.src = base64;
        img.setAttribute("data-dgg-preview-src", el.href);
        img.style.maxHeight="300px";
        img.style.maxWidth="300px";
        img.style.marginLeft="5px";
        img.style.marginBottom="10px";
        img.style.marginTop="10px";
        img.style.display="block";
        img.style.cursor="pointer";

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

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

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

        const chatEl = getChatEl();
        const onReady = () => { if (shouldStickToBottom) requestAnimationFrame(()=>{ chatEl.scrollTop = chatEl.scrollHeight; }); };

        if ("decode" in img && typeof img.decode === "function"){
          img.decode().then(onReady).catch(onReady);
        } else {
          img.addEventListener("load", onReady, { once:true });
          img.addEventListener("error", onReady, { once:true });
        }

        if (parent) {
          parent.appendChild(img);
          if (config.HideLink) el.remove();
        }
        inFlight.delete(el);
      };
    }
  });
}

const chatObserver = new MutationObserver((mutations) => {
  for (const m of mutations) {
    for (const n of m.addedNodes) {
      if (n.nodeType !== 1 || !n.querySelector) continue; // ELEMENT_NODE
      // Only pick *unprocessed* links
      const links = n.querySelectorAll('.externallink:not([data-dgg-previewed="1"])');
      links.forEach(handleLink);
    }
  }
});

console.log("[DGG Img Preview] Connecting");
waitForElm(".chat-lines").then((chatEl) => {
  // Overlay
  overlay = document.createElement("div");
  overlay.style.position="fixed";
  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);

  // Sticky-bottom engine
  shouldStickToBottom = isAtBottom(chatEl);
  chatEl.addEventListener("scroll", () => { shouldStickToBottom = isAtBottom(chatEl); }, { passive:true });

  const ro = new ResizeObserver(() => { if (shouldStickToBottom) scrollToBottom(chatEl); });
  ro.observe(chatEl);

  // Observe chat
  chatObserver.observe(chatEl, { attributes:false, childList:true, characterData:false, subtree:true });

  console.log("[DGG Img Preview] Connected");
  console.log("[DGG Img Preview] Adding Settings");
  addSettings();
});