// ==UserScript==
// @name DGG Auto Image Embed (Release 09/07/25 - dedupe+container)
// @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. Robust de-duplication.
// @match https://www.destiny.gg/embed/chat*
// @icon https://cdn.destiny.gg/2.49.0/emotes/6296cf7e8ccd0.png
// @version 10.2.3
// @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 priv = `#${keyName}`;
Object.defineProperty(this, key, {
set(v){ this[priv]=v; unsafeWindow.localStorage.setItem(`${prefix}${keyName}`, JSON.stringify(v)); },
get(){ if (this[priv]===undefined){ const raw=unsafeWindow.localStorage.getItem(`${prefix}${keyName}`); this[priv]=raw!=null?JSON.parse(raw):defaultValue; } return this[priv]; }
});
}
}
}
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 area = document.querySelector("#chat-settings-form");
if (!area) { console.warn("[DGG Img Preview] settings form not found"); return; }
const title = document.createElement("h4"); title.textContent = "D.GG Img Preview Settings";
const mk = (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;
};
area.append(title, mk("Blur nsfw/nsfl images","BlurNSFW"), mk("Hide link after preview","HideLink"), mk("Disable 4chan (i.4cdn.org) auto-embeds","Block4cdn"));
console.log("[DGG Img Preview] Settings Added");
}
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 });
});
}
// --------- de-dupe state ---------
// Track links currently being fetched (DOM node guard)
const inFlight = new WeakSet();
// Track per-message processed URLs: WeakMap<messageContainerEl, Set<href>>
const messageSeen = new WeakMap();
// Find the message container (direct child of .chat-lines) for a given node
function findMessageContainer(node){
const chatEl = getChatEl();
let cur = node;
// climb until direct child of chatEl, or stop at <body>
while (cur && cur.parentNode && cur.parentNode !== chatEl){
cur = cur.parentNode;
}
// if we reached a direct child of chatEl use it, else fallback to the closest element ancestor
if (cur && cur.parentNode === chatEl) return cur;
return node.closest ? (node.closest(".chat-line, .msg, .message") || node.parentElement || node) : node;
}
function alreadySeen(container, href){
let set = messageSeen.get(container);
if (!set){ set = new Set(); messageSeen.set(container, set); }
if (set.has(href)) return true;
set.add(href);
return false;
}
function handleLink(el){
// Link-level guards
if (el.dataset.dggPreviewed === "1" || inFlight.has(el)) return;
if (config.Block4cdn && /https?:\/\/i\.4cdn\.org\//i.test(el.href)) return;
if (!imageRegex.test(el.href)) return;
// Message-level guard
const container = findMessageContainer(el);
if (alreadySeen(container, el.href)) return;
// Mark immediately to stop subsequent observer passes
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 this message already has a preview for this href, bail
if (container && container.querySelector && container.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 });
}
// Append directly after the link so we stay inside this message container
if (el.parentNode) {
el.parentNode.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 target 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();
});