// ==UserScript==
// @name Flashii Chat - Ultreme Script
// @namespace https://patchii.net/lester/flashii-chat-userscripts
// @version 5.0
// @description Better quotes & delete button, quote blocks, upload progress bar, and Go to Forum button with settings.
// @author lester
// @match *://chat.flashii.net/*
// @grant none
// @downloadURL https://patchii.net/lester/flashii-chat-userscripts/raw/branch/trunk/ultreme-script.user.js
// @updateURL https://patchii.net/lester/flashii-chat-userscripts/raw/branch/trunk/ultreme-script.meta.js
// ==/UserScript==
(() => {
const STORAGE_PREFIX = "ultreme_";
const loadBoolSetting = (key, def) => {
try {
const v = localStorage.getItem(STORAGE_PREFIX + key);
if (v === null) return def;
return v === "1";
} catch {
return def;
}
};
const saveBoolSetting = (key, val) => {
try {
localStorage.setItem(STORAGE_PREFIX + key, val ? "1" : "0");
} catch {}
};
let showQuoteButton = loadBoolSetting("showQuoteButton", true);
let showGotoButton = loadBoolSetting("showGotoButton", true);
let enableDelete = loadBoolSetting("enableDelete", true);
let showDeleteButton = loadBoolSetting("showDeleteButton", true);
let enableQuoteBlocks = loadBoolSetting("enableQuoteBlocks", true);
let enableUploadProgress = loadBoolSetting("enableUploadProgress", true);
let enableForumButton = loadBoolSetting("enableForumButton", true);
let selectedText = "";
let pendingQuote = null;
let previewInterval = null;
let selectedMessageIndex = -1;
const cssID = "chat-style";
const processedMessages = new WeakSet();
let mediaModal = null;
let userscriptPrevButton = null;
let userscriptPrevMenu = null;
const NativeXHR = window.XMLHttpRequest;
const uploads = new Map();
function createProgressBar() {
if (document.getElementById("upload-progress-wrapper")) return;
const spoilerBtn = [...document.querySelectorAll(".markup__button")].find(
(btn) => btn.textContent.trim().toLowerCase() === "spoiler",
);
if (!spoilerBtn) return;
const wrapper = document.createElement("div");
wrapper.id = "upload-progress-wrapper";
wrapper.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
margin-left: 10px;
opacity: 0;
transition: opacity 0.15s ease;
`;
const label = document.createElement("span");
label.textContent = "Uploading File...";
label.style.cssText = `
color: var(--theme-colour-main-colour);
font-size: 13px;
`;
const barContainer = document.createElement("div");
barContainer.style.cssText = `
position: relative;
width: 100px;
height: 20px;
background-color: var(--theme-colour-input-menu-button);
border-radius: 2px;
box-shadow: 0 0 0 1px var(--theme-colour-input-menu-box-shadow);
overflow: hidden;
`;
const inner = document.createElement("div");
inner.id = "upload-progress-inner";
inner.style.cssText = `
background-color: var(--theme-colour-input-menu-button-hover);
height: 100%;
width: 0%;
transition: width 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
color: var(--theme-colour-main-colour);
font-size: 12px;
font-weight: bold;
font-family: sans-serif;
`;
barContainer.appendChild(inner);
wrapper.append(label, barContainer);
spoilerBtn.parentElement.appendChild(wrapper);
}
function updateCombinedProgress() {
let totalLoaded = 0;
let totalSize = 0;
let allDone = true;
for (const { loaded, total, done } of uploads.values()) {
totalLoaded += loaded;
totalSize += total;
if (!done) allDone = false;
}
const percent =
totalSize === 0 ? 0 : Math.round((totalLoaded / totalSize) * 100);
const wrapper = document.getElementById("upload-progress-wrapper");
const inner = document.getElementById("upload-progress-inner");
if (!wrapper || !inner) return;
wrapper.style.opacity = "1";
inner.style.width = `${percent}%`;
inner.textContent = `${percent}%`;
if (allDone) {
setTimeout(() => {
wrapper.style.opacity = "0";
setTimeout(() => {
inner.style.width = "0%";
inner.textContent = "";
uploads.clear();
}, 150);
}, 800);
}
}
function UploadXHR() {
const xhr = new NativeXHR();
xhr.open = function (method, url) {
this._isUpload = method === "POST" && url.includes("/uploads");
return NativeXHR.prototype.open.apply(this, arguments);
};
xhr.send = function (body) {
if (this._isUpload && enableUploadProgress) {
const id = Math.random().toString(36).slice(2);
createProgressBar();
uploads.set(id, { loaded: 0, total: 0, done: false });
this.upload.onprogress = (e) => {
if (e.lengthComputable) {
uploads.set(id, {
loaded: e.loaded,
total: e.total,
done: false,
});
updateCombinedProgress();
}
};
this.addEventListener("loadend", () => {
const current = uploads.get(id);
if (current) {
uploads.set(id, { ...current, done: true });
updateCombinedProgress();
}
});
}
return NativeXHR.prototype.send.apply(this, arguments);
};
return xhr;
}
function installUploadProgress() {
if (window.XMLHttpRequest === UploadXHR) return;
window.XMLHttpRequest = UploadXHR;
}
function uninstallUploadProgress() {
if (window.XMLHttpRequest === UploadXHR) {
window.XMLHttpRequest = NativeXHR;
}
const wrapper = document.getElementById("upload-progress-wrapper");
if (wrapper) wrapper.remove();
uploads.clear();
}
if (enableUploadProgress) {
installUploadProgress();
}
function addForumButton() {
if (!enableForumButton) return;
const sidebarSelector = document.querySelector(".sidebar__selector");
const firstButton =
sidebarSelector?.querySelector(".sidebar__selector-mode");
if (
sidebarSelector &&
firstButton &&
!sidebarSelector.querySelector(".custom-button")
) {
const newButton = document.createElement("button");
newButton.classList.add("sidebar__selector-mode", "custom-button");
newButton.title = "Go to Forum";
newButton.style.width = "40px";
newButton.style.height = "40px";
newButton.style.border = "none";
newButton.style.backgroundColor = "transparent";
newButton.style.cursor = "pointer";
newButton.style.display = "flex";
newButton.style.alignItems = "center";
newButton.style.justifyContent = "center";
const favicon =
document.querySelector('link[rel~="icon"]')?.href || "/favicon.ico";
const faviconImg = document.createElement("img");
faviconImg.src = favicon;
faviconImg.alt = "Flashii Forum";
faviconImg.style.width = "32px";
faviconImg.style.height = "32px";
newButton.appendChild(faviconImg);
newButton.addEventListener("click", (event) => {
event.preventDefault();
window.open("https://flashii.net/forum", "_blank");
});
sidebarSelector.insertBefore(newButton, firstButton);
}
}
function removeForumButton() {
const btn = document.querySelector(".custom-button");
if (btn) btn.remove();
}
function smartSlice(text, limit = 100, edge = 50, spill = 10) {
const isWhiteSpace = (char) => /\s/.test(char || "");
const n = text.length;
if (n <= limit) return text;
if (n <= limit + spill) {
if (!isWhiteSpace(text[limit]) && !isWhiteSpace(text[limit - 1]))
return text;
}
let startEnd = Math.min(edge, n);
if (!isWhiteSpace(text[startEnd]) && !isWhiteSpace(text[startEnd - 1])) {
const before = text.lastIndexOf(" ", startEnd);
const after = text.indexOf(" ", startEnd);
if (
before !== -1 &&
(after === -1 || startEnd - before <= after - startEnd)
) {
startEnd = before;
} else if (after !== -1) {
startEnd = after;
}
}
if (startEnd <= 0) startEnd = Math.min(edge, n);
const start = text.slice(0, startEnd).trimEnd();
let endStart = Math.max(n - edge, 0);
if (!isWhiteSpace(text[endStart]) && !isWhiteSpace(text[endStart - 1])) {
const after = text.indexOf(" ", endStart);
const before = text.lastIndexOf(" ", endStart);
if (after !== -1) {
endStart = after + 1;
} else if (before !== -1) {
endStart = before + 1;
} else {
endStart = Math.max(n - edge, 0);
}
}
const end = text.slice(endStart).trimStart();
return `${start} ... ${end}`;
}
function cleanMessage(msg) {
let msgText = msg || "";
const endQuoteIdx = msgText.lastIndexOf("[/quote]");
if (endQuoteIdx !== -1) msgText = msgText.slice(endQuoteIdx + 8).trim();
return msgText
.replace(/\[Embed\]|\[Remove\]/g, "")
.replace(
/\[color=var\(--theme-colour-message-time-colour\)\][^\w\[\]]{1,3}\[\/color\]/g,
"",
)
.trim();
}
function ensureMediaModal() {
if (mediaModal) return;
mediaModal = document.createElement("div");
mediaModal.id = "umi-media-modal";
mediaModal.innerHTML = `
`;
mediaModal.addEventListener("click", () => {
mediaModal.style.display = "none";
const content = mediaModal.querySelector(".umi-media-modal-content");
if (content) content.innerHTML = "";
});
document.body.appendChild(mediaModal);
}
function openMediaModal(url) {
ensureMediaModal();
const content = mediaModal.querySelector(".umi-media-modal-content");
if (!content) return;
content.innerHTML = "";
const cleanUrl = url.split("#")[0];
const extMatch = cleanUrl.split("?")[0].match(/\.([a-z0-9]+)$/i);
const ext = extMatch ? extMatch[1].toLowerCase() : "";
const videoExts = ["mp4", "webm", "ogg"];
const imgExts = ["png", "jpg", "jpeg", "gif", "webp"];
if (videoExts.includes(ext)) {
const video = document.createElement("video");
video.src = url;
video.controls = true;
video.autoplay = true;
video.loop = true;
video.style.maxWidth = "100%";
video.style.maxHeight = "100%";
content.appendChild(video);
} else if (imgExts.includes(ext) || ext === "") {
const img = document.createElement("img");
img.src = url;
img.alt = "Media preview";
img.style.maxWidth = "100%";
img.style.maxHeight = "100%";
content.appendChild(img);
} else {
window.open(url, "_blank", "noopener,noreferrer");
return;
}
mediaModal.style.display = "flex";
}
document.addEventListener("mouseup", () => {
const sel = window.getSelection();
const range = sel?.rangeCount ? sel.getRangeAt(0) : null;
const inside =
range?.commonAncestorContainer?.closest?.("#umi-messages") ||
range?.commonAncestorContainer?.parentElement?.closest?.("#umi-messages");
if (inside) selectedText = sel.toString().trim();
});
document.addEventListener("click", (e) => {
if (
e.target.matches("button.markup__button") &&
e.target.textContent.trim().toLowerCase() === "quote"
) {
setTimeout(() => {
const input = document.querySelector("textarea.input__text");
if (input && selectedText) {
const quoted = `[quote]${selectedText}[/quote]`;
input.value =
input.value.trim() === "[quote][/quote]"
? quoted
: input.value + quoted;
input.focus();
selectedText = "";
}
}, 50);
}
});
window.addEventListener("umi:connect", () => {
const uid = Umi.User.getCurrentUser().id;
document.body.dataset.ultremeQuoteBlocks = enableQuoteBlocks ? "1" : "0";
const rgbToHex = (rgb) => {
const m = rgb?.match(/\d+/g);
return m?.length >= 3
? "#" +
m
.slice(0, 3)
.map((x) => (+x).toString(16).padStart(2, "0"))
.join("")
: "#000";
};
const getRelativeTime = (dateString) => {
if (!dateString) return "";
const createdDate = new Date(dateString);
const now = new Date();
const diffMs = now - createdDate;
const seconds = Math.floor(diffMs / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) return `${seconds}s ago`;
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ${minutes % 60}m ago`;
return `${days}d ${hours % 24}h ${minutes % 60}m ago`;
};
const injectCSS = () => {
if (document.getElementById(cssID)) return;
const style = document.createElement("style");
style.id = cssID;
style.textContent = `
.message .message-button-container {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
display: flex;
gap: 4px;
}
.message .quote-button,
.message .delete-button,
.message .goto-button {
opacity: 0;
border: none;
border-radius: 2px;
padding: 1px 6px;
font-size: 13px;
cursor: pointer;
background: var(--theme-colour-input-menu-button);
color: var(--theme-colour-main-colour);
transition: opacity 0.15s ease;
}
.message .quote-button:hover,
.message .delete-button:hover,
.message .goto-button:hover {
background: var(--theme-colour-input-menu-button-hover);
}
.message .quote-button:active,
.message .delete-button:active,
.message .goto-button:active {
background: var(--theme-colour-input-menu-button-active);
}
.message:hover .quote-button,
.message:hover .delete-button,
.message:hover .goto-button {
opacity: 1;
}
#quote-preview {
background: var(--theme-colour-input-background);
border: 1px solid var(--theme-colour-settings-input-border);
padding: 6px 10px;
font-size: 13px;
margin: 4px -1px;
display: flex;
align-items: flex-start;
max-width: 100%;
}
#quote-preview span {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
}
#cancel-quote {
flex: 0 0 auto;
margin-left: 6px;
background: none;
border: none;
color: var(--theme-colour-main-colour);
cursor: pointer;
font-weight: bold;
padding: 0 6px;
font-size: 13px;
align-self: flex-start;
}
.highlight-temp {
animation: blinkOutline 1s ease-in-out;
outline: 2px solid transparent;
border-radius: 4px;
outline-offset: 1px;
}
@keyframes blinkOutline {
0% { outline-color: transparent; }
50% { outline-color: var(--theme-colour-main-accent); }
100% { outline-color: transparent; }
}
.message__time,
.message__text i {
cursor: pointer;
}
.message__time:hover,
.message__text i:hover {
text-decoration: underline;
}
.message.selected-quote::after {
content: "";
position: absolute;
inset: 0;
opacity: 0.1;
background-color: color-mix(in srgb, var(--theme-colour-main-accent) 50%, black);
}
.message__container.has-inline-quote {
display: flex;
align-items: flex-start;
}
.message__container.has-inline-quote .message__meta {
flex: 0 0 auto;
}
.message__container.has-inline-quote .message__body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.message.message--first .message__container.has-inline-quote {
flex-direction: column;
}
.message.message--first .message__container.has-inline-quote .message__meta {
margin-right: 0;
margin-bottom: 2px;
}
.message:not(.message--first) .message__container.has-inline-quote {
flex-direction: row;
}
.message__quote {
margin-top: 2px;
margin-bottom: 2px;
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
border: 2px solid var(--theme-colour-settings-input-border);
background: var(--theme-colour-input-background);
cursor: pointer;
opacity: 0.97;
display: flex;
align-items: flex-start;
gap: 6px;
max-width: 100%;
box-sizing: border-box;
}
.message__quote:hover {
border-color: var(--theme-colour-main-accent);
}
.message__quote-avatar {
width: 20px;
height: 20px;
border-radius: 0;
flex: 0 0 auto;
object-fit: cover;
}
.message__quote-body {
flex: 1 1 auto;
min-width: 0;
}
.message__quote-header {
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.message__quote-text {
white-space: normal;
word-break: break-word;
}
.message__quote-text a.message__quote-media {
color: var(--theme-colour-main-accent);
text-decoration: underline;
cursor: pointer;
margin-right: 4px;
}
.message__quote + .message__text {
display: block;
margin-top: 2px;
}
#umi-media-modal {
display: none;
position: fixed;
inset: 0;
z-index: 9999;
align-items: center;
justify-content: center;
}
#umi-media-modal .umi-media-modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
}
#umi-media-modal .umi-media-modal-content {
max-width: 90%;
max-height: 90%;
}
#umi-media-modal img,
#umi-media-modal video {
max-width: 100%;
max-height: 100%;
display: block;
box-shadow: 0 0 16px rgba(0, 0, 0, 0.8);
}
body[data-ultreme-quote-blocks="1"] .ultreme-inline-quote {
display: none !important;
}
body[data-ultreme-quote-blocks="0"] .message__quote {
display: none !important;
}
.setting__hint {
margin-bottom: 4px;
font-size: 12px;
color: var(--theme-colour-message-time-colour);
}
`;
document.head.appendChild(style);
};
const setupUserscriptSettingsTab = () => {
const menusRoot = document.querySelector(".sidebar__menus");
const selector = document.querySelector(".sidebar__selector");
if (!menusRoot || !selector) return;
if (menusRoot.querySelector(".sidebar__menu--userscript")) return;
const userscriptMenu = document.createElement("div");
userscriptMenu.className = "sidebar__menu hidden";
userscriptMenu.innerHTML = `
`;
menusRoot.appendChild(userscriptMenu);
const settingsBtn = [...selector.querySelectorAll(".sidebar__selector-mode")].find(
(b) => b.title === "Settings" || b.querySelector(".fa-cog"),
);
const userscriptBtn = document.createElement("button");
userscriptBtn.type = "button";
userscriptBtn.className = "sidebar__selector-mode";
userscriptBtn.title = "Ultreme Script Settings";
userscriptBtn.innerHTML =
'';
if (settingsBtn && settingsBtn.nextSibling) {
settingsBtn.parentNode.insertBefore(userscriptBtn, settingsBtn.nextSibling);
} else {
selector.insertBefore(userscriptBtn, selector.firstChild);
}
const showQuoteButtonInput = userscriptMenu.querySelector(
".js-ultreme-showQuoteButton",
);
const showGotoButtonInput = userscriptMenu.querySelector(
".js-ultreme-showGotoButton",
);
const enableQuoteBlocksInput = userscriptMenu.querySelector(
".js-ultreme-enableQuoteBlocks",
);
const enableDeleteInput = userscriptMenu.querySelector(
".js-ultreme-enableDelete",
);
const showDeleteButtonInput = userscriptMenu.querySelector(
".js-ultreme-showDeleteButton",
);
const enableUploadProgressInput = userscriptMenu.querySelector(
".js-ultreme-enableUploadProgress",
);
const enableForumButtonInput = userscriptMenu.querySelector(
".js-ultreme-enableForumButton",
);
if (showQuoteButtonInput) {
showQuoteButtonInput.checked = showQuoteButton;
showQuoteButtonInput.addEventListener("change", () => {
showQuoteButton = showQuoteButtonInput.checked;
saveBoolSetting("showQuoteButton", showQuoteButton);
document.querySelectorAll(".quote-button").forEach((btn) => {
btn.style.display = showQuoteButton ? "" : "none";
});
});
}
if (showGotoButtonInput) {
showGotoButtonInput.checked = showGotoButton;
showGotoButtonInput.addEventListener("change", () => {
showGotoButton = showGotoButtonInput.checked;
saveBoolSetting("showGotoButton", showGotoButton);
document.querySelectorAll(".goto-button").forEach((btn) => {
btn.style.display = showGotoButton ? "" : "none";
});
});
}
if (enableQuoteBlocksInput) {
enableQuoteBlocksInput.checked = enableQuoteBlocks;
enableQuoteBlocksInput.addEventListener("change", () => {
enableQuoteBlocks = enableQuoteBlocksInput.checked;
saveBoolSetting("enableQuoteBlocks", enableQuoteBlocks);
document.body.dataset.ultremeQuoteBlocks = enableQuoteBlocks ? "1" : "0";
if (enableQuoteBlocks) {
const msgs = document.querySelectorAll("#umi-messages .message");
msgs.forEach((msg) => {
const textEl =
msg.querySelector(".message__text") ||
msg.querySelector(".message-tiny-text");
if (textEl) enhanceScriptQuote(msg, textEl);
});
}
});
}
if (enableDeleteInput) {
enableDeleteInput.checked = enableDelete;
enableDeleteInput.addEventListener("change", () => {
enableDelete = enableDeleteInput.checked;
saveBoolSetting("enableDelete", enableDelete);
document.querySelectorAll(".delete-button").forEach((btn) => {
btn.style.display =
enableDelete && showDeleteButton ? "" : "none";
});
});
}
if (showDeleteButtonInput) {
showDeleteButtonInput.checked = showDeleteButton;
showDeleteButtonInput.addEventListener("change", () => {
showDeleteButton = showDeleteButtonInput.checked;
saveBoolSetting("showDeleteButton", showDeleteButton);
document.querySelectorAll(".delete-button").forEach((btn) => {
btn.style.display =
enableDelete && showDeleteButton ? "" : "none";
});
});
}
if (enableUploadProgressInput) {
enableUploadProgressInput.checked = enableUploadProgress;
enableUploadProgressInput.addEventListener("change", () => {
enableUploadProgress = enableUploadProgressInput.checked;
saveBoolSetting("enableUploadProgress", enableUploadProgress);
if (enableUploadProgress) {
installUploadProgress();
} else {
uninstallUploadProgress();
}
});
}
if (enableForumButtonInput) {
enableForumButtonInput.checked = enableForumButton;
enableForumButtonInput.addEventListener("change", () => {
enableForumButton = enableForumButtonInput.checked;
saveBoolSetting("enableForumButton", enableForumButton);
if (enableForumButton) {
addForumButton();
} else {
removeForumButton();
}
});
}
const menus = () => menusRoot.querySelectorAll(".sidebar__menu");
const buttons = () => selector.querySelectorAll(".sidebar__selector-mode");
userscriptBtn.addEventListener("click", () => {
const allMenus = menus();
const allButtons = buttons();
const isActive = userscriptBtn.classList.contains(
"sidebar__selector-mode--active",
);
if (!isActive) {
const currentBtn = [...allButtons].find(
(b) =>
b !== userscriptBtn &&
b.classList.contains("sidebar__selector-mode--active"),
);
const currentMenu = [...allMenus].find(
(m) => !m.classList.contains("hidden"),
);
userscriptPrevButton = currentBtn || null;
userscriptPrevMenu = currentMenu || null;
allButtons.forEach((b) =>
b.classList.toggle("sidebar__selector-mode--active", b === userscriptBtn),
);
allMenus.forEach((m) => m.classList.add("hidden"));
userscriptMenu.classList.remove("hidden");
} else {
userscriptBtn.classList.remove("sidebar__selector-mode--active");
userscriptMenu.classList.add("hidden");
if (userscriptPrevButton && userscriptPrevMenu) {
userscriptPrevButton.classList.add("sidebar__selector-mode--active");
userscriptPrevMenu.classList.remove("hidden");
}
}
});
buttons().forEach((b) => {
if (b === userscriptBtn) return;
b.addEventListener("click", () => {
userscriptBtn.classList.remove("sidebar__selector-mode--active");
userscriptMenu.classList.add("hidden");
});
});
};
const showQuotePreview = ({ name, msg, color, created }) => {
clearInterval(previewInterval);
let preview = document.getElementById("quote-preview");
if (!preview) {
preview = document.createElement("div");
preview.id = "quote-preview";
const span = document.createElement("span");
const cancel = document.createElement("button");
cancel.id = "cancel-quote";
cancel.textContent = "×";
cancel.title = "Cancel quote";
cancel.onclick = () => {
pendingQuote = null;
preview.remove();
clearInterval(previewInterval);
clearSelectedMessage();
};
preview.append(span, cancel);
const form = document.querySelector("form.input");
const menus = form?.querySelector(".input__menus");
const main = form?.querySelector(".input__main");
if (menus && main) form.insertBefore(preview, main);
}
const span = preview.querySelector("span");
const cleanMsg = cleanMessage(msg);
const updateTime = () => {
const time = getRelativeTime(created);
const displayText = smartSlice(cleanMsg, 100, 50, 10);
span.innerHTML = `Quoting ${name} @ ${time} — ${displayText}`;
};
updateTime();
previewInterval = setInterval(updateTime, 1000);
};
const applyQuote = (messageData) => {
pendingQuote = messageData;
showQuotePreview(pendingQuote);
const input = document.querySelector("textarea.input__text");
if (input) input.focus();
};
const getVisibleMessages = () => {
const container = document.getElementById("umi-messages");
return [...container.children].filter((msg) =>
msg.classList.contains("message"),
);
};
const clearSelectedMessage = () => {
const messages = getVisibleMessages();
messages.forEach((msg) => msg.classList.remove("selected-quote"));
selectedMessageIndex = -1;
};
const selectMessage = (index) => {
const messages = getVisibleMessages();
clearSelectedMessage();
if (index >= 0 && index < messages.length) {
selectedMessageIndex = index;
messages[index].classList.add("selected-quote");
const messageData = extractMessageData(messages[index]);
if (messageData) {
applyQuote(messageData);
}
} else {
pendingQuote = null;
const preview = document.getElementById("quote-preview");
if (preview) preview.remove();
clearInterval(previewInterval);
}
};
const extractMessageData = (msg) => {
const userEl = msg.querySelector(".message__user");
const textEl =
msg.querySelector(".message__text") ||
msg.querySelector(".message-tiny-text");
const timeEl = msg.querySelector(".message__time");
const dataCreated = msg.getAttribute("data-created");
const raw = userEl?.style?.color;
const userColor = !raw || raw === "inherit" ? null : rgbToHex(raw);
const name = userEl?.textContent?.trim() || "Unknown";
const msgText = cleanMessage(
msg.dataset.body || textEl?.textContent.trim() || "",
);
if (!userEl || !textEl || !timeEl) return null;
return {
name,
color: userColor,
created: dataCreated,
id: msg.dataset.id,
msg: msgText,
};
};
const enhanceScriptQuote = (msg, textEl) => {
if (!textEl || msg.dataset.enhancedQuote === "1") return;
const anchor = textEl.querySelector('a[href^="#"]');
const qEl = textEl.querySelector("q");
const metaI = textEl.querySelector("i");
if (!anchor || !qEl || !metaI) return;
const aText = anchor.textContent || "";
if (aText.length !== 1 || aText.charCodeAt(0) !== 0x200c) return;
const href = anchor.getAttribute("href") || "";
const idMatch = href.match(/^#(\d{17})$/);
const targetId = idMatch ? idMatch[1] : null;
const arrowSpan = [...textEl.querySelectorAll("span")].find((s) =>
s.textContent.includes("└"),
);
const br = textEl.querySelector("br");
metaI.classList.add("ultreme-inline-quote");
anchor.classList.add("ultreme-inline-quote");
qEl.classList.add("ultreme-inline-quote");
if (arrowSpan) arrowSpan.classList.add("ultreme-inline-quote");
if (br) br.classList.add("ultreme-inline-quote");
let targetMsg = null;
if (targetId) {
targetMsg = document.getElementById(`message-${targetId}`);
}
const quoteContainer = document.createElement("div");
quoteContainer.className = "message__quote";
const avatarImg = document.createElement("img");
avatarImg.className = "message__quote-avatar";
avatarImg.alt = "";
if (targetMsg) {
const authorId = targetMsg.dataset?.author;
if (authorId) {
avatarImg.src = `https://flashii.net/assets/avatar/${authorId}?res=80`;
}
}
let quotedName = "Unknown";
let quotedColor = null;
let quotedCreated = null;
let relTime = "";
if (metaI) {
const headerText = metaI.textContent || "";
const atIdx = headerText.indexOf("@ ");
const dashIdx = headerText.lastIndexOf("—");
if (atIdx !== -1) {
const namePart = headerText.slice(0, atIdx).trim();
if (namePart) quotedName = namePart;
}
if (atIdx !== -1 && dashIdx !== -1 && dashIdx > atIdx + 2) {
relTime = headerText.slice(atIdx + 2, dashIdx).trim();
}
}
if (targetMsg) {
const targetUserEl = targetMsg.querySelector(".message__user");
const rawColor = targetUserEl?.style?.color;
const targetName = targetUserEl?.textContent?.trim();
if (targetName) quotedName = targetName;
if (rawColor && rawColor !== "inherit") {
quotedColor = rawColor.startsWith("#") ? rawColor : rgbToHex(rawColor);
}
quotedCreated = targetMsg.getAttribute("data-created");
} else if (metaI) {
const nameB = metaI.querySelector("b");
const colourSource =
metaI.querySelector('span[style*="color"]') ||
nameB?.parentElement ||
nameB;
const rawColor =
colourSource && colourSource.style
? colourSource.style.color
: null;
if (rawColor && rawColor !== "inherit") {
quotedColor = rawColor.startsWith("#") ? rawColor : rgbToHex(rawColor);
}
}
if (!relTime && quotedCreated) {
relTime = getRelativeTime(quotedCreated);
}
const body = document.createElement("div");
body.className = "message__quote-body";
const header = document.createElement("div");
header.className = "message__quote-header";
header.innerHTML = `${
quotedColor
? `${quotedName}`
: `${quotedName}`
}${
relTime
? ` @ ${relTime}`
: ""
}`;
const textDiv = document.createElement("div");
textDiv.className = "message__quote-text";
const rawQuoted = qEl.textContent || "";
const withoutEmbed = rawQuoted.replace(/\[Embed\]/gi, "").trim();
const urlPattern = /(https?:)?\/\/\S+/g;
const hasUrl = urlPattern.test(withoutEmbed);
urlPattern.lastIndex = 0;
if (hasUrl) {
let lastIndex = 0;
let match;
const re = new RegExp(urlPattern);
while ((match = re.exec(withoutEmbed)) !== null) {
const urlStart = match.index;
const urlEnd = re.lastIndex;
const before = withoutEmbed.slice(lastIndex, urlStart);
if (before) {
textDiv.appendChild(document.createTextNode(before));
}
let url = match[0];
if (!/^https?:\/\//i.test(url)) {
url = "https:" + url;
}
const link = document.createElement("a");
link.href = url;
link.textContent = "Media...";
link.className = "message__quote-media";
link.addEventListener("click", (ev) => {
ev.preventDefault();
openMediaModal(url);
});
textDiv.appendChild(link);
lastIndex = urlEnd;
}
const tail = withoutEmbed.slice(lastIndex);
if (tail) {
textDiv.appendChild(document.createTextNode(tail));
}
} else {
textDiv.textContent = withoutEmbed;
}
body.appendChild(header);
body.appendChild(textDiv);
if (avatarImg.src) {
quoteContainer.appendChild(avatarImg);
}
quoteContainer.appendChild(body);
const container = msg.querySelector(".message__container");
const meta = msg.querySelector(".message__meta");
if (container) {
container.classList.add("has-inline-quote");
let bodyWrapper = container.querySelector(".message__body");
if (!bodyWrapper) {
bodyWrapper = document.createElement("div");
bodyWrapper.className = "message__body";
if (textEl && textEl.parentNode === container) {
container.insertBefore(bodyWrapper, textEl);
bodyWrapper.appendChild(textEl);
} else {
container.appendChild(bodyWrapper);
}
}
if (textEl && textEl.parentNode === bodyWrapper) {
bodyWrapper.insertBefore(quoteContainer, textEl);
} else {
bodyWrapper.insertBefore(quoteContainer, bodyWrapper.firstChild);
}
} else if (meta && meta.parentNode) {
meta.parentNode.insertBefore(quoteContainer, meta.nextSibling);
}
if (targetId) {
quoteContainer.addEventListener("click", (ev) => {
if (
ev.target &&
ev.target.closest &&
ev.target.closest(".message__quote-media")
) {
return;
}
const target = document.getElementById(`message-${targetId}`);
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "center" });
target.classList.add("highlight-temp");
setTimeout(
() => target.classList.remove("highlight-temp"),
1500,
);
}
});
}
msg.dataset.enhancedQuote = "1";
};
const processMessage = (msg, input, uid) => {
if (processedMessages.has(msg)) return;
processedMessages.add(msg);
const id = msg.dataset.id;
const author = msg.dataset.author;
const body = msg.dataset.body || "";
if (
!input ||
["has disconnected", "has joined"].some((p) => body.includes(p))
)
return;
const textEl =
msg.querySelector(".message__text") ||
msg.querySelector(".message-tiny-text");
const userEl = msg.querySelector(".message__user");
const timeEl = msg.querySelector(".message__time");
const dataCreated = msg.getAttribute("data-created");
const raw = userEl?.style?.color;
const userColor = !raw || raw === "inherit" ? null : rgbToHex(raw);
const name = userEl?.textContent?.trim() || "Unknown";
let msgText = cleanMessage(
msg.dataset.body || textEl?.textContent.trim() || "",
);
if (timeEl) {
timeEl.onclick = (e) => {
if (enableDelete && e.shiftKey && author === uid) {
Umi.Server.SendMessage(`/delete ${id}`);
} else {
clearSelectedMessage();
msg.classList.add("selected-quote");
applyQuote({
name,
color: userColor,
created: dataCreated,
id,
msg: msgText,
});
}
};
}
const relTimeEl = textEl?.querySelector("i");
if (relTimeEl) {
relTimeEl.onclick = () => {
const anchor = textEl.querySelector('a[href^="#"]');
const idMatch = anchor?.getAttribute("href")?.match(/^#(\d{17})$/);
if (idMatch) {
const targetId = idMatch[1];
const target = document.getElementById(`message-${targetId}`);
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "center" });
target.classList.add("highlight-temp");
setTimeout(
() => target.classList.remove("highlight-temp"),
1500,
);
}
}
};
}
let btnContainer = msg.querySelector(".message-button-container");
if (!btnContainer) {
btnContainer = document.createElement("div");
btnContainer.className = "message-button-container";
msg.style.position = "relative";
msg.appendChild(btnContainer);
}
if (!msg.querySelector(".delete-button") && author === uid) {
const del = document.createElement("button");
del.className = "delete-button";
del.innerHTML = "×";
del.title = "Delete this message";
del.onclick = () => Umi.Server.SendMessage(`/delete ${id}`);
del.style.display =
enableDelete && showDeleteButton ? "" : "none";
btnContainer.appendChild(del);
}
if (!msg.querySelector(".quote-button") && textEl && userEl && timeEl) {
const btn = document.createElement("button");
btn.className = "quote-button";
btn.textContent = "Quote";
btn.title = "Quote this message";
btn.onclick = () => {
clearSelectedMessage();
msg.classList.add("selected-quote");
applyQuote({
name,
color: userColor,
created: dataCreated,
id,
msg: msgText,
});
};
btn.style.display = showQuoteButton ? "" : "none";
btnContainer.appendChild(btn);
}
if (
!msg.querySelector(".goto-button") &&
textEl?.querySelector('a[href^="#"]')
) {
const anchor = textEl.querySelector('a[href^="#"]');
const idMatch = anchor?.getAttribute("href")?.match(/^#(\d{17})$/);
if (idMatch) {
const targetId = idMatch[1];
const go = document.createElement("button");
go.className = "goto-button";
go.textContent = "Go to quoted";
go.title = "Scroll to quoted message";
go.onclick = () => {
const target = document.getElementById(`message-${targetId}`);
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "center" });
target.classList.add("highlight-temp");
setTimeout(
() => target.classList.remove("highlight-temp"),
1500,
);
}
};
go.style.display = showGotoButton ? "" : "none";
btnContainer.appendChild(go);
}
}
enhanceScriptQuote(msg, textEl);
};
const processNewMessages = () => {
const input = document.querySelector("textarea.input__text");
const container = document.getElementById("umi-messages");
const children = [...container.children];
for (let i = children.length - 1; i >= 0; i--) {
const msg = children[i];
if (msg.classList.contains("message")) {
if (processedMessages.has(msg)) break;
processMessage(msg, input, uid);
}
}
};
injectCSS();
setupUserscriptSettingsTab();
processNewMessages();
addForumButton();
new MutationObserver(processNewMessages).observe(
document.getElementById("umi-messages"),
{ childList: true },
);
const form = document.querySelector("form.input");
const input = document.querySelector("textarea.input__text");
if (form && input && !form.__quoteIntercepted) {
form.__quoteIntercepted = true;
form.addEventListener(
"submit",
() => {
if (pendingQuote) {
const { name, color, id, msg, created } = pendingQuote;
const hidden = "\u200C";
const cleanMsg = cleanMessage(msg);
const time = getRelativeTime(created);
const storedText = smartSlice(cleanMsg, 100, 50, 10);
const quoteBlock = `[i]${
color ? `[color=${color}]` : ""
}[b]${name}[/b]${
color ? "[/color]" : ""
} [color=var(--theme-colour-message-time-colour)]@ ${time} —[/color][/i][url=#${id}]${hidden}[/url] [quote]${storedText}[/quote]`;
const full = `${quoteBlock}${
input.value
? `\n[color=var(--theme-colour-message-time-colour)]└─[/color] ` +
input.value.trimStart()
: ""
}`;
input.value = full;
pendingQuote = null;
const preview = document.getElementById("quote-preview");
if (preview) preview.remove();
clearInterval(previewInterval);
clearSelectedMessage();
}
},
true,
);
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && pendingQuote) {
e.preventDefault();
pendingQuote = null;
clearSelectedMessage();
const preview = document.getElementById("quote-preview");
if (preview) preview.remove();
clearInterval(previewInterval);
} else if (e.ctrlKey && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
e.preventDefault();
const messages = getVisibleMessages();
if (messages.length === 0) return;
if (e.key === "ArrowUp") {
if (selectedMessageIndex === -1) {
selectMessage(messages.length - 1);
} else if (selectedMessageIndex > 0) {
selectMessage(selectedMessageIndex - 1);
}
} else if (e.key === "ArrowDown") {
if (selectedMessageIndex === -1) {
return;
} else if (selectedMessageIndex < messages.length - 1) {
selectMessage(selectedMessageIndex + 1);
} else {
selectMessage(-1);
}
}
}
});
});
})();