// ==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();
});