NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Enhanced Chat Unit3D
// @version 2.1.6
// @description Chat functionalities: Reply, Message and Gift buttons. BBCode buttons. Toggle menu.
// @updateURL https://openuserjs.org/meta/ZukoXZoku/Enhanced_Chat_Unit3D.meta.js
// @downloadURL https://openuserjs.org/install/ZukoXZoku/Enhanced_Chat_Unit3D.user.js
// @icon https://ptpimg.me/883q39.png
// @match https://fearnopeer.com/
// @match https://upload.cx/
// @match https://aither.cc/
// @match https://oldtoons.world/
// @match https://reelflix.cc/
// @match https://blutopia.cc/
// @match https://darkpeers.org/
// @match https://luminarr.me/
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
// Additional credits
// ZukoXZoku@OTW
if (window.__U3D_ENHANCED_CHAT_INIT__) {
// prevent double init
}
else {
window.__U3D_ENHANCED_CHAT_INIT__ = true;
GM_addStyle(`
:root {
--u3d-bg:#0f172a;
--u3d-surface:#1e2738;
--u3d-surface2:#2a3550;
--u3d-border:#3f495f;
--u3d-text:#eaeaea;
--u3d-muted:#a7b0c4;
--u3d-link:#8cc0ff;
--u3d-link-hover:#cfe3ff;
--u3d-reply-1:#2dd4bf; --u3d-reply-2:#14b8a6;
--u3d-dm-1:#60a5fa; --u3d-dm-2:#3b82f6;
--u3d-gift-1:#f59e0b; --u3d-gift-2:#ec4899;
--u3d-action-radius:8px;
}
.fa-solid.fa-gear:hover { animation: spin 1.3s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
#chatbox_header.u3d-hide-actions .panel__action,
.u3d-chat-header.u3d-hide-actions .panel__action { display: none !important; }
#chatbox_header { position: relative; border-radius: 15px 15px 0 0; }
#chatbox_header .panel__heading { border-radius: 15px 15px 0 0; color: var(--u3d-text); }
.u3d-settings-anchor { margin-right: 5px; z-index: 1; padding: 7px; }
#settingsButton {
cursor: pointer; color: var(--u3d-text); display: inline-flex; align-items: center; justify-content: center;
font-size: 16px; width: 28px; height: 28px; border-radius: 6px; background: transparent;
}
#settingsButton:hover { background: rgba(255,255,255,0.08); }
#settingsPanel {
display: none; position: absolute; top: calc(100% + 8px); right: 0;
background: var(--u3d-surface); border-radius: 12px; border: 1px solid var(--u3d-border);
padding: 16px; color: var(--u3d-text); z-index: 10011; width: 360px; font-family: 'Segoe UI', sans-serif;
box-shadow: 0 8px 24px rgba(0,0,0,0.35); font-size: 14px;
}
#settingsPanel .title { color: var(--u3d-text); display:flex; justify-content:space-between; align-items:center; }
#settingsPanel .title .ver { font-size: 12px; color: var(--u3d-muted); }
#settingsPanel .row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; gap: 10px; }
#settingsPanel .row .label { min-width: 110px; color: var(--u3d-muted); }
#settingsPanel .row input[type="number"], #settingsPanel .row input[type="text"], #settingsPanel .row select, #settingsPanel .row button, #settingsPanel .row input[type="color"] {
background: var(--u3d-surface2); border:1px solid var(--u3d-border); color: var(--u3d-text); border-radius:6px;
padding:6px 8px; cursor: pointer;
}
#settingsPanel .row .inline { display:flex; gap:8px; align-items:center; flex-wrap: wrap; }
#settingsPanel .section { border: 1px solid var(--u3d-border); border-radius: 10px; padding: 10px; margin: 10px 0; }
#settingsPanel .section .section-title { font-weight: 700; margin-bottom: 6px; color: var(--u3d-text); }
.switch { position: relative; display: inline-block; width: 38px; height: 20px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background-color: #444c64; border-radius: 20px; transition: 0.3s; }
.slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px;
background-color: var(--u3d-text); border-radius: 50%; transition: 0.3s; }
.switch input:checked + .slider { background-color: #5b43d1; }
.switch input:checked + .slider:before { transform: translateX(18px); }
.u3d-btn {
cursor: pointer; background: var(--u3d-surface2); color: var(--u3d-text); border: 1px solid var(--u3d-border); border-radius: 8px;
padding: 6px 10px; font-size: 13px;
}
.u3d-btn:hover { background: #344261; }
.bbcode-button {
cursor: pointer; padding: 6px 10px 2px 10px; border-radius: 8px; color: var(--u3d-text); transition: 0.2s ease;
display: inline-flex; align-items: center; justify-content: center; min-width: 34px; min-height: 30px; border: 1px solid transparent;
}
.bbcode-button:hover { background-color: var(--u3d-surface2); border-color: var(--u3d-border); }
#bbCodesPanelShell { position: relative; display: inline-flex; width: 100%; }
#bbCodesPanelShell > div { width: 100%; }
.u3d-bb-more { position: relative; }
#bbCodeDropdown {
color: var(--u3d-text); display: inline-flex; align-items: center; justify-content: center;
padding: 8px 10px; border-radius: 8px; transition: 0.2s ease; cursor: pointer;
}
#bbCodeDropdown:hover { background: var(--u3d-surface2); border: 1px solid var(--u3d-border); }
#bbCodeDropdown.open { background: var(--u3d-surface2); }
#bbCodeDropdown i { transition: transform 0.2s ease; }
#bbCodeDropdown.open i { transform: rotate(90deg); }
#bbCodeDropdownMenu {
position: absolute; top: calc(100% + 8px); background: #151d29; padding: 10px; border: 1px solid var(--u3d-border);
border-radius: 10px; box-shadow: 0 6px 16px rgba(0,0,0,0.45); z-index: 10015; display: none;
width: max-content; min-width: 260px; max-width: 560px;
}
#bbCodeDropdownMenu.open { display: block; }
#bbCodeDropdownMenu .grid {
display: grid; grid-template-columns: repeat(8, 36px); gap: 8px; align-items: center; justify-items: center;
}
#bbCodeDropdownMenu .bbcode-button { width: 36px; height: 32px; padding: 0; }
.u3d-action-bar { display: inline-flex; gap: 6px; margin-left: 6px; align-items: center; vertical-align: middle; }
.u3d-action-btn {
cursor: pointer; width: 28px; height: 28px; border-radius: var(--u3d-action-radius); display: inline-flex; align-items: center; justify-content: center;
color: var(--u3d-text); background: transparent; border: 1px solid transparent;
transition: transform .15s ease, box-shadow .2s ease, background .2s ease, color .2s ease;
}
.u3d-action-btn:hover { transform: translateY(-1px); }
html.u3d-action-fancy .u3d-action-btn--reply { background: linear-gradient(135deg,var(--u3d-reply-1),var(--u3d-reply-2)); color: #041512; }
html.u3d-action-fancy .u3d-action-btn--dm { background: linear-gradient(135deg,var(--u3d-dm-1),var(--u3d-dm-2)); color: #031226; }
html.u3d-action-fancy .u3d-action-btn--gift { background: linear-gradient(135deg,var(--u3d-gift-1),var(--u3d-gift-2)); color: #170a03; }
html:not(.u3d-action-fancy) .u3d-action-btn:hover { background: rgba(255,255,255,.08); }
html.u3d-actions-hover .u3d-action-bar { opacity: 0; pointer-events: none; transition: opacity .15s ease; }
html.u3d-actions-hover .chatbox-message:hover .u3d-action-bar,
html.u3d-actions-hover .message:hover .u3d-action-bar { opacity: 1; pointer-events: auto; }
#settingsThemes .radio-group { display: flex; flex-wrap: wrap; gap: 10px; }
#settingsThemes .radio-group label { background: rgba(255,255,255,0.04); border: 1px solid var(--u3d-border); padding: 6px 10px; border-radius: 8px; cursor:pointer; }
#settingsThemes .radio-group input { margin-right: 6px; }
#settingsThemes .palette { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#settingsThemes .palette .row { justify-content: space-between; }
#settingsThemes .buttons { display: grid; gap: 10px; grid-template-columns: repeat(4, 1fr); padding: 10px; }
.u3d-textarea { width: 100%; min-height: 160px; resize: vertical; background: #0d1422; color: var(--u3d-text);
border: 1px solid var(--u3d-border); border-radius: 8px; padding: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12.5px; line-height: 1.45;
}
#settingsChangelog .changelog-entry { border: 1px solid var(--u3d-border); border-radius: 10px; padding: 10px; margin-bottom: 10px; }
#settingsChangelog .changelog-entry .ver { font-weight: 700; color: var(--u3d-text); margin-bottom: 6px; }
#settingsChangelog .changelog-entry ul { margin: 0 0 0 18px; padding: 0; color: var(--u3d-muted); }
.chatbox-message__menu {
margin: 1px 10px 0 0;
padding: 0;
display: inline;
}
.chatbox-message__delete-button {
color: #91919185;
margin: 5px;
font-size: inherit !important;
}
.chatbox-message__delete-button .fa.fa-trash {
color: #a01919;
}
.chatbox-message__delete-button .fa.fa-trash:hover {
color: #ca1717;
}
`);
(function () {
"use strict";
const SCRIPT_VERSION = "2.1.6";
const SELECTORS = {
chatbox: ['#chatbox__messages-create', '#chatbox__message', 'textarea[name="message"]', 'textarea#message'],
messages: ['.chatroom__messages', '.chatbox__messages', '.chatroom-messages'],
header: ['#chatbox_header', '.chatbox__header', '.chatroom__header']
};
const K = {
TOGGLE_BBCODES: "toggleBBCodes",
TOGGLE_MSG_GIFT: "toggleMessageGift",
TOGGLE_REPLY: "toggleReply",
TOGGLE_PANEL: "togglePanel",
AUTO_DELETE_ENABLED: "autoDeleteEnabled",
AUTO_DELETE_INTERVAL: "autoDeleteIntervalSec",
ACTIONS_HOVER: "u3dActionsHover",
FANCY_ACTIONS: "u3dFancyActions",
THEME_ACTIVE: "u3dThemeActive",
THEME_CUSTOM: "u3dThemeCustom",
USERCSS_ENABLED: "u3dUserCssEnabled",
USERCSS_CODE: "u3dUserCssCode"
};
const DEFAULTS = {
[K.TOGGLE_BBCODES]: true,
[K.TOGGLE_MSG_GIFT]: true,
[K.TOGGLE_REPLY]: true,
[K.TOGGLE_PANEL]: false,
[K.AUTO_DELETE_ENABLED]: false,
[K.AUTO_DELETE_INTERVAL]: 5,
[K.ACTIONS_HOVER]: false,
[K.FANCY_ACTIONS]: false,
[K.THEME_ACTIVE]: "default",
[K.THEME_CUSTOM]: JSON.stringify({
bg: "#0f172a",
surface: "#1e2738",
surface2: "#2a3550",
border: "#3f495f",
text: "#eaeaea",
muted: "#a7b0c4",
link: "#8cc0ff",
linkHover: "#cfe3ff",
reply1: "#2dd4bf",
reply2: "#14b8a6",
dm1: "#60a5fa",
dm2: "#3b82f6",
gift1: "#f59e0b",
gift2: "#ec4899",
radius: 8
}),
[K.USERCSS_ENABLED]: false,
[K.USERCSS_CODE]: `/* Custom CSS (example)
#chatbox_header .panel__heading { text-transform: none; }
*/`
};
const THEMES = {
default: {
bg: "#0f172a",
surface: "#1e2738",
surface2: "#2a3550",
border: "#3f495f",
text: "#eaeaea",
muted: "#a7b0c4",
link: "#8cc0ff",
linkHover: "#cfe3ff",
reply1: "#2dd4bf",
reply2: "#14b8a6",
dm1: "#60a5fa",
dm2: "#3b82f6",
gift1: "#f59e0b",
gift2: "#ec4899",
radius: 8
},
ocean: {
bg: "#0b1220",
surface: "#0f1a2e",
surface2: "#15223c",
border: "#21304a",
text: "#e7f6ff",
muted: "#b2c9e0",
link: "#6ed3ff",
linkHover: "#bfeaff",
reply1: "#34d399",
reply2: "#0ea5e9",
dm1: "#67e8f9",
dm2: "#3b82f6",
gift1: "#22c55e",
gift2: "#06b6d4",
radius: 8
},
neon: {
bg: "#120f20",
surface: "#1c1530",
surface2: "#261c46",
border: "#3a2f63",
text: "#f0eaff",
muted: "#cbbff0",
link: "#b794f4",
linkHover: "#e9ddff",
reply1: "#22d3ee",
reply2: "#a78bfa",
dm1: "#f472b6",
dm2: "#fb7185",
gift1: "#facc15",
gift2: "#34d399",
radius: 10
},
rose: {
bg: "#1d1216",
surface: "#28171c",
surface2: "#341a21",
border: "#50303a",
text: "#ffeef3",
muted: "#f3c3cf",
link: "#ff9fc0",
linkHover: "#ffd1e0",
reply1: "#fda4af",
reply2: "#f43f5e",
dm1: "#f5d0fe",
dm2: "#e879f9",
gift1: "#fecaca",
gift2: "#fb7185",
radius: 10
},
midnight: {
bg: "#0c0f14",
surface: "#121720",
surface2: "#18202c",
border: "#243043",
text: "#e4ecf5",
muted: "#aab8cc",
link: "#7fb6ff",
linkHover: "#cfe0ff",
reply1: "#1f2937",
reply2: "#374151",
dm1: "#0ea5e9",
dm2: "#3b82f6",
gift1: "#8b5cf6",
gift2: "#22d3ee",
radius: 8
}
};
const CHANGELOG = [{
ver: "2.1.6",
items: [
"Added Luminarr",
"Removed Lst.gg, because it changed the UI"
]
}
];
const MIN_INTERVAL_SEC = 1;
const MAX_DELETE_PER_TICK = 12;
const CUSTOM_COLOR = "#4a2d84";
const bbCodesPanelID = "bbCodesPanel";
const settingsButtonID = "settingsButton";
const settingsPanelID = "settingsPanel";
let autoDeleteTimerId = null;
let userCssStyleEl = null;
const gChat = {
box: null,
messages: null,
header: null
};
const BBCODES_PANEL_HTML = `
<div id="${bbCodesPanelID}" style="position: relative; display: flex; flex-wrap: wrap; gap: 6px; background-color: #0000; border-radius: 8px; z-index: 1; margin: 10px 0 0 0; padding: 6px;">
<span class="bbcode-button" data-bbcode="[img][/img]" title="Image"><i class="fa-solid fa-image"></i></span>
<span class="bbcode-button" data-bbcode="[url][/url]" title="Link"><i class="fa-solid fa-link"></i></span>
<span class="bbcode-button" data-bbcode="[b][/b]" title="Bold"><i class="fa-solid fa-bold"></i></span>
<span class="bbcode-button" data-bbcode="[i][/i]" title="Italic"><i class="fa-solid fa-italic"></i></span>
<span class="bbcode-button" data-bbcode="[u][/u]" title="Underline"><i class="fa-solid fa-underline"></i></span>
<div class="u3d-bb-more">
<span id="bbCodeDropdown" class="bbcode-button" title="More"><i class="fa-solid fa-angle-right"></i></span>
<div id="bbCodeDropdownMenu">
<div class="grid">
<span class="bbcode-button" data-bbcode="[code][/code]" title="Code"><i class="fa-solid fa-code"></i></span>
<span class="bbcode-button" data-bbcode="[spoiler][/spoiler]" title="Spoiler"><i class="fa-solid fa-eye-slash"></i></span>
<span class="bbcode-button" data-bbcode="[quote][/quote]" title="Quote"><i class="fa-solid fa-quote-left"></i></span>
<span class="bbcode-button" data-action="color" title="Text color"><i class="fa-solid fa-palette"></i></span>
<span class="bbcode-button" data-bbcode="[youtube][/youtube]" title="YouTube"><i class="fa-brands fa-youtube"></i></span>
<span class="bbcode-button" data-bbcode="[s][/s]" title="Strike"><i class="fa-solid fa-strikethrough"></i></span>
<span class="bbcode-button" data-bbcode="[left][/left]" title="Align left"><i class="fa-solid fa-align-left"></i></span>
<span class="bbcode-button" data-bbcode="[center][/center]" title="Align center"><i class="fa-solid fa-align-center"></i></span>
<span class="bbcode-button" data-bbcode="[right][/right]" title="Align right"><i class="fa-solid fa-align-right"></i></span>
<span class="bbcode-button" data-action="size" title="Text size"><i class="fa-solid fa-text-height"></i></span>
<span class="bbcode-button" data-action="list-ul" title="Unordered List"><i class="fa-solid fa-list-ul"></i></span>
<span class="bbcode-button" data-action="list-ol" title="Ordered List"><i class="fa-solid fa-list-ol"></i></span>
<span class="bbcode-button" data-action="list-item" title="[*]"><i class="fa-solid fa-asterisk"></i></span>
<span class="bbcode-button" data-action="hr" title="Horizontal Line"><i class="fa-solid fa-minus"></i></span>
<span class="bbcode-button" data-bbcode="/msg" title="/msg username message"><i class="fa-solid fa-envelope"></i></span>
<span class="bbcode-button" data-bbcode="/gift" title="/gift username number message"><i class="fa-solid fa-gift"></i></span>
</div>
</div>
</div>
</div>`;
// Utils
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const loadSetting = (key) => {
const v = localStorage.getItem(key);
if (v === null || v === undefined) return DEFAULTS[key];
if (v === "true") return true;
if (v === "false") return false;
if (!isNaN(v) && v.trim() !== "") return Number(v);
return v;
};
const saveSetting = (key, value) => localStorage.setItem(key, String(value));
const toInt = (val, def) => {
const n = parseInt(val, 10);
return Number.isFinite(n) ? n : def;
};
function waitForAny(selectors, interval = 300, timeout = 20000) {
return new Promise((resolve, reject) => {
const start = Date.now();
const tick = () => {
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) return resolve(el);
}
if (Date.now() - start > timeout) return reject(new Error(`Timeout: ${selectors.join(", ")}`));
setTimeout(tick, interval);
};
tick();
});
}
function getMessageUserInfo(msg) {
let a = msg.querySelector('.chatbox-message__header .user-tag__link, .message-header .user-tag__link, .chatbox-message__address .user-tag__link, .user-tag__link');
if (!a) a = msg.querySelector('a[href*="/users/"], a[href*="/user/"], a[href*="/profile/"]');
let name = "",
profileUrl = "";
if (a) {
name = (a.textContent || "").trim();
if (a.href) profileUrl = new URL(a.href, location.origin).href;
}
return {
name,
profileUrl
};
}
function findDeleteButton(msg) {
return msg.querySelector("button.chatbox-message__delete-button") || null;
}
function isVisible(el) {
if (!el) return false;
const st = window.getComputedStyle(el);
return st.display !== "none" && st.visibility !== "hidden" && el.offsetParent !== null;
}
// Theme
function getActiveThemeObject() {
const active = String(loadSetting(K.THEME_ACTIVE) || "default");
if (active === "custom") {
try {
return JSON.parse(loadSetting(K.THEME_CUSTOM) || "{}") || THEMES.default;
}
catch {
return THEMES.default;
}
}
return THEMES[active] || THEMES.default;
}
function applyThemeFromObj(t) {
const root = document.documentElement;
root.style.setProperty('--u3d-bg', t.bg);
root.style.setProperty('--u3d-surface', t.surface);
root.style.setProperty('--u3d-surface2', t.surface2);
root.style.setProperty('--u3d-border', t.border);
root.style.setProperty('--u3d-text', t.text);
root.style.setProperty('--u3d-muted', t.muted);
root.style.setProperty('--u3d-link', t.link);
root.style.setProperty('--u3d-link-hover', t.linkHover);
root.style.setProperty('--u3d-reply-1', t.reply1);
root.style.setProperty('--u3d-reply-2', t.reply2);
root.style.setProperty('--u3d-dm-1', t.dm1);
root.style.setProperty('--u3d-dm-2', t.dm2);
root.style.setProperty('--u3d-gift-1', t.gift1);
root.style.setProperty('--u3d-gift-2', t.gift2);
root.style.setProperty('--u3d-action-radius', (t.radius || 8) + 'px');
}
function applyThemeFromSettings() {
applyThemeFromObj(getActiveThemeObject());
}
// Custom CSS
function ensureUserCssStyleEl() {
if (!userCssStyleEl) {
userCssStyleEl = document.createElement("style");
userCssStyleEl.id = "u3d-user-css";
document.head.appendChild(userCssStyleEl);
}
return userCssStyleEl;
}
function applyUserCssIfEnabled() {
const enabled = !!loadSetting(K.USERCSS_ENABLED);
const css = String(loadSetting(K.USERCSS_CODE) || "");
ensureUserCssStyleEl().textContent = enabled ? css : "";
}
// Auto-delete
function stopAutoDeleteTimer() {
if (autoDeleteTimerId) {
clearInterval(autoDeleteTimerId);
autoDeleteTimerId = null;
}
}
function deleteVisibleMessagesOnce() {
if (!gChat.messages) return;
let deleted = 0;
const messages = gChat.messages.querySelectorAll(".chatbox-message, .message");
for (const msg of messages) {
if (deleted >= MAX_DELETE_PER_TICK) break;
const delBtn = findDeleteButton(msg);
if (!delBtn || !isVisible(delBtn) || msg.dataset.u3dAutodeleting === "1") continue;
msg.dataset.u3dAutodeleting = "1";
try {
delBtn.click();
deleted++;
}
catch (e) {
delete msg.dataset.u3dAutodeleting;
}
}
}
function startAutoDeleteTimer() {
stopAutoDeleteTimer();
if (!loadSetting(K.AUTO_DELETE_ENABLED)) return;
const intervalSec = Math.max(MIN_INTERVAL_SEC, toInt(loadSetting(K.AUTO_DELETE_INTERVAL), 5));
autoDeleteTimerId = setInterval(() => {
try {
deleteVisibleMessagesOnce();
}
catch {}
}, intervalSec * 1000);
try {
deleteVisibleMessagesOnce();
}
catch {}
}
// Actions
function buildActionBar() {
const bar = document.createElement("div");
bar.className = "u3d-action-bar";
const mkBtn = (cls, title, iconHtml) => {
const b = document.createElement("span");
b.className = `u3d-action-btn ${cls}`;
b.title = title;
b.innerHTML = iconHtml;
return b;
};
const replyBtn = mkBtn("u3d-action-btn--reply", "Reply", '<i class="fa-solid fa-reply"></i>');
const dmBtn = mkBtn("u3d-action-btn--dm", "Direct Message", '<i class="fa-solid fa-envelope"></i>');
const giftBtn = mkBtn("u3d-action-btn--gift", "Gift", '<i class="fa-solid fa-gift"></i>');
bar.append(replyBtn, dmBtn, giftBtn);
return bar;
}
function setupReplyFeatures() {
const newMessageTextArea = gChat.box;
const processed = new WeakSet();
function quoteMessage(name, profileUrl, rawMessage) {
const clean = (s) => (s || "").replace(/\r?\n+/g, " ").replace(/\s+/g, " ").trim();
let displayName = clean(name).replace(/\bUnknown\b/gi, "").replace(/\s{2,}/g, " ").trim();
if (!displayName && profileUrl) {
try {
const u = new URL(profileUrl, location.origin);
const parts = u.pathname.split("/").filter(Boolean);
displayName = clean(decodeURIComponent(parts[parts.length - 1] || ""));
}
catch {}
}
if (!displayName) return;
const label = profileUrl ? `[url=${profileUrl}]${displayName}[/url]` : displayName;
const msg = clean(rawMessage);
const quote = `[b]${label} :[/b][color=#999999] "[i]${msg}[/i]"[/color]\n\n`;
newMessageTextArea.setRangeText(quote, newMessageTextArea.selectionStart, newMessageTextArea.selectionEnd, "end");
newMessageTextArea.focus();
}
function addIconsIfNeeded(message) {
if (!(message instanceof Element) || processed.has(message)) return;
const header = message.querySelector(".chatbox-message__header, .message-header");
if (header && !header.querySelector(".u3d-action-bar")) {
const bar = buildActionBar();
header.appendChild(bar);
const showMG = !!loadSetting(K.TOGGLE_MSG_GIFT);
const showReply = !!loadSetting(K.TOGGLE_REPLY);
bar.querySelector(".u3d-action-btn--reply").style.display = showReply ? "inline-flex" : "none";
bar.querySelector(".u3d-action-btn--dm").style.display = showMG ? "inline-flex" : "none";
bar.querySelector(".u3d-action-btn--gift").style.display = showMG ? "inline-flex" : "none";
}
processed.add(message);
}
$$(".chatbox-message, .message").forEach(addIconsIfNeeded);
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.matches(".chatbox-message, .message")) addIconsIfNeeded(node);
node.querySelectorAll?.(".chatbox-message, .message").forEach(addIconsIfNeeded);
}
}
});
if (gChat.messages) observer.observe(gChat.messages, {
childList: true,
subtree: true
});
gChat.messages?.addEventListener("click", (e) => {
const action = e.target.closest(".u3d-action-btn");
if (!action) return;
const msgEl = action.closest(".chatbox-message, .message");
const {
name,
profileUrl
} = getMessageUserInfo(msgEl);
if (!name) return;
const content = msgEl.querySelector(".chatbox-message__content, .message-content")?.innerText || "";
if (action.classList.contains("u3d-action-btn--reply")) {
quoteMessage(name, profileUrl, content);
}
else if (action.classList.contains("u3d-action-btn--dm") || action.classList.contains("u3d-action-btn--gift")) {
const nameNode = msgEl.querySelector(".user-tag__name");
const clean = (s) => (s || "")
.replace(/\r?\n/g, " ")
.replace(/\s+/g, " ")
.replace(/\bUnknown\b/gi, "")
.trim();
let uname = nameNode ? nameNode.textContent : name;
uname = clean(uname);
// Fallback: derive from profile URL path if needed
if (!uname && profileUrl) {
try {
const u = new URL(profileUrl, location.origin);
const parts = u.pathname.split("/").filter(Boolean);
uname = clean(decodeURIComponent(parts[parts.length - 1] || ""));
} catch {}
}
if (!uname) return;
const cmd = `${action.classList.contains("u3d-action-btn--dm") ? "/msg" : "/gift"} ${uname}`;
newMessageTextArea.value = cmd; // replace entire content (avoids leftover “Unknown”)
newMessageTextArea.setSelectionRange(cmd.length, cmd.length);
newMessageTextArea.focus();
newMessageTextArea.dispatchEvent(new Event("input", { bubbles: true }));
}
});
}
// BBCode helpers
function splitBB(bb) {
const startTag = bb.substring(0, bb.indexOf("]") + 1);
const endTag = bb.substring(bb.lastIndexOf("["));
return {
startTag,
endTag
};
}
function insertBBCode(chatbox, bbCode) {
const selStart = chatbox.selectionStart,
selEnd = chatbox.selectionEnd;
const selected = chatbox.value.substring(selStart, selEnd);
const {
startTag,
endTag
} = splitBB(bbCode);
const toInsert = selected ? (startTag + selected + endTag + " ") : (startTag + endTag + " ");
chatbox.setRangeText(toInsert, selStart, selEnd, "end");
chatbox.focus();
}
function insertBBCodeWithClipboard(tag, chatbox) {
const {
startTag,
endTag
} = splitBB(tag);
navigator.clipboard.readText().then(clipText => {
const content = (clipText || "").trim();
chatbox.setRangeText(startTag + content + endTag + " ", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
}).catch(() => {
chatbox.setRangeText(startTag + endTag + " ", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
});
}
function insertImgBBCodeWithClipboard(tag, chatbox) {
const {
startTag,
endTag
} = splitBB(tag);
navigator.clipboard.readText().then(clipText => {
const content = (clipText || "").trim();
chatbox.setRangeText(startTag + content + endTag + "\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
}).catch(() => {
chatbox.setRangeText(startTag + endTag + "\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
});
}
function insertWrapWithArg(chatbox, tagBase, promptText, defaultVal) {
const selStart = chatbox.selectionStart,
selEnd = chatbox.selectionEnd;
const selected = chatbox.value.substring(selStart, selEnd);
const arg = window.prompt(promptText, defaultVal);
if (arg === null) return;
const open = `[${tagBase}=${arg}]`,
close = `[/${tagBase}]`;
const toInsert = selected ? (open + selected + close + " ") : (open + close + " ");
chatbox.setRangeText(toInsert, selStart, selEnd, "end");
chatbox.focus();
}
function parseYouTubeId(input) {
if (!input) return null;
const s = input.trim();
if (/^[A-Za-z0-9_-]{11}$/.test(s)) return s;
let urlStr = s;
if (!/^https?:\/\//i.test(urlStr)) urlStr = "https://" + urlStr;
try {
const u = new URL(urlStr);
if (/youtu\.be$/.test(u.hostname)) {
const seg = u.pathname.split("/").filter(Boolean)[0];
if (/^[A-Za-z0-9_-]{11}$/.test(seg)) return seg;
}
const v = u.searchParams.get("v");
if (v && /^[A-Za-z0-9_-]{11}$/.test(v)) return v;
const parts = u.pathname.split("/").filter(Boolean);
if (parts.length >= 2 && ["shorts", "embed", "v", "live", "watch"].includes(parts[0])) {
const cand = parts[1].split(/[?#&]/)[0];
if (/^[A-Za-z0-9_-]{11}$/.test(cand)) return cand;
}
}
catch {}
return null;
}
function insertYouTubeBBCodeSmart(tag, chatbox) {
const {
startTag,
endTag
} = splitBB(tag);
const insertId = (id) => {
if (!id) return;
chatbox.setRangeText(startTag + id + endTag + "\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
};
const selected = chatbox.value.substring(chatbox.selectionStart, chatbox.selectionEnd).trim();
let id = parseYouTubeId(selected);
if (id) return insertId(id);
navigator.clipboard.readText().then(clip => {
const got = parseYouTubeId(clip || "");
if (got) insertId(got);
else {
const manual = prompt("Paste YouTube link or 11-char video ID:");
insertId(parseYouTubeId(manual || ""));
}
}).catch(() => {
const manual = prompt("Paste YouTube link or 11-char video ID:");
insertId(parseYouTubeId(manual || ""));
});
}
function settingsPanelHTML() {
const sw = (id, label) => `
<div class="row">
<div class="label">${label}</div>
<label class="switch">
<input type="checkbox" id="${id}">
<span class="slider"></span>
</label>
</div>`;
return `
<div id="${settingsPanelID}">
<div class="title">
<div>Unit3D Chatbox Enhanced</div>
<div class="ver">Version ${SCRIPT_VERSION}</div>
</div>
<div id="settingsHome">
<div class="section">
<div class="section-title">Settings</div>
${sw("toggleBBCodes", "BBCodes")}
${sw("toggleMessageGift", "Direct Message - Gift buttons")}
${sw("toggleReply", "Reply button")}
${sw("togglePanel", "Show top Panel buttons")}
</div>
<div class="section">
<div class="section-title">Auto-delete</div>
${sw("autoDeleteToggle", "Auto-delete messages")}
<div class="row">
<div class="label">Time (seconds)</div>
<div class="inline">
<input type="number" id="autoDeleteInterval" min="1" step="1" value="5" style="cursor: text;">
</div>
</div>
</div>
<div class="section">
<div class="section-title">Actions</div>
${sw("fancyActions", "Fancy action buttons")}
${sw("actionsHover", "Show action buttons on hover")}
</div>
<div class="row" style="justify-content:flex-end; gap:8px;">
<button id="openThemesBtn" class="u3d-btn" type="button" style="cursor: pointer">Themes</button>
<button id="openChangelogBtn" class="u3d-btn" type="button" style="cursor: pointer">Changelog</button>
</div>
</div>
<div id="settingsThemes" style="display:none;">
<div class="section">
<div class="section-title">Theme Presets</div>
<div class="radio-group">
${Object.keys(THEMES).map(n => `<label><input type="radio" name="themePreset" value="${n}"> ${n}</label>`).join("")}
<label><input type="radio" name="themePreset" value="custom"> custom</label>
</div>
</div>
<div class="section" id="themeCustomEditor" style="display:none;">
<div class="section-title">Custom Palette</div>
<div class="palette">
<div class="row"><div class="label">Background</div><div class="inline"><input type="color" id="tBg"></div></div>
<div class="row"><div class="label">Surface</div><div class="inline"><input type="color" id="tSurface"></div></div>
<div class="row"><div class="label">Surface 2</div><div class="inline"><input type="color" id="tSurface2"></div></div>
<div class="row"><div class="label">Border</div><div class="inline"><input type="color" id="tBorder"></div></div>
<div class="row"><div class="label">Text</div><div class="inline"><input type="color" id="tText"></div></div>
<div class="row"><div class="label">Muted</div><div class="inline"><input type="color" id="tMuted"></div></div>
<div class="row"><div class="label">Link</div><div class="inline"><input type="color" id="tLink"></div></div>
<div class="row"><div class="label">Link Hover</div><div class="inline"><input type="color" id="tLinkHover"></div></div>
<div class="row"><div class="label">Reply gradient</div><div class="inline"><input type="color" id="tReply1"><input type="color" id="tReply2"></div></div>
<div class="row"><div class="label">DM gradient</div><div class="inline"><input type="color" id="tDm1"><input type="color" id="tDm2"></div></div>
<div class="row"><div class="label">Gift gradient</div><div class="inline"><input type="color" id="tGift1"><input type="color" id="tGift2"></div></div>
<div class="row"><div class="label">Action radius (px)</div><div class="inline"><input type="number" id="tRadius" min="4" max="20" step="1" value="8"></div></div>
</div>
<div class="buttons">
<button id="themeApplyBtn" class="u3d-btn" type="button">Apply</button>
<button id="themeSaveBtn" class="u3d-btn" type="button">Save as Custom</button>
<button id="themeResetBtn" class="u3d-btn" type="button">Reset default</button>
</div>
</div>
<div class="section">
<div class="section-title">Custom CSS</div>
<div class="row">
<div class="label">Enable custom CSS</div>
<label class="switch">
<input type="checkbox" id="userCssEnable">
<span class="slider"></span>
</label>
</div>
<div class="row">
<div class="label">CSS code</div>
<div style="flex:1;">
<textarea id="userCssText" class="u3d-textarea" spellcheck="false"></textarea>
</div>
</div>
<div class="buttons">
<button id="userCssApplyBtn" class="u3d-btn" type="button">Apply</button>
<button id="userCssSaveBtn" class="u3d-btn" type="button">Save</button>
<button id="userCssDefaultBtn" class="u3d-btn" type="button">Load default</button>
<button id="userCssExportBtn" class="u3d-btn" type="button">Export CSS</button>
<button id="userCssImportBtn" class="u3d-btn" type="button">Import CSS</button>
<button id="userCssClearBtn" class="u3d-btn" type="button">Clear</button>
</div>
</div>
<div class="row" style="justify-content:flex-end; gap:8px;">
<button id="backFromThemesBtn" class="u3d-btn" type="button">Back</button>
</div>
</div>
<div id="settingsChangelog" style="display:none;">
<div class="section">
<div class="section-title">Changelog</div>
<div id="changelogContent"></div>
</div>
<div class="row" style="justify-content:flex-end; gap:8px;">
<button id="backFromChangelogBtn" class="u3d-btn" type="button">Back</button>
</div>
</div>
</div>
`;
}
function showSettingsView(view) {
const home = document.getElementById("settingsHome");
const themes = document.getElementById("settingsThemes");
const log = document.getElementById("settingsChangelog");
if (!home || !themes || !log) return;
home.style.display = (view === "home") ? "block" : "none";
themes.style.display = (view === "themes") ? "block" : "none";
log.style.display = (view === "changelog") ? "block" : "none";
}
function renderChangelog() {
const wrap = document.getElementById("changelogContent");
if (!wrap) return;
wrap.innerHTML = CHANGELOG.map(c => `
<div class="changelog-entry">
<div class="ver">v${c.ver}</div>
<ul>${c.items.map(i => `<li>${i}</li>`).join("")}</ul>
</div>
`).join("");
}
function applySettings() {
const toggleBBCodes = !!loadSetting(K.TOGGLE_BBCODES);
const toggleMessageGift = !!loadSetting(K.TOGGLE_MSG_GIFT);
const toggleReply = !!loadSetting(K.TOGGLE_REPLY);
const togglePanel = !!loadSetting(K.TOGGLE_PANEL);
const autoDeleteEnabled = !!loadSetting(K.AUTO_DELETE_ENABLED);
const intervalSec = Math.max(MIN_INTERVAL_SEC, toInt(loadSetting(K.AUTO_DELETE_INTERVAL), 5));
if (gChat.header) {
gChat.header.classList.add("u3d-chat-header");
gChat.header.classList.toggle("u3d-hide-actions", !togglePanel);
}
const bbToggle = document.getElementById("toggleBBCodes");
if (bbToggle) bbToggle.checked = toggleBBCodes;
const bbShell = document.getElementById("bbCodesPanelShell");
if (bbShell) bbShell.style.display = toggleBBCodes ? "inline-flex" : "none";
const mgToggle = document.getElementById("toggleMessageGift");
if (mgToggle) mgToggle.checked = toggleMessageGift;
const replyToggle = document.getElementById("toggleReply");
if (replyToggle) replyToggle.checked = toggleReply;
$$(".u3d-action-btn--reply").forEach(el => el.style.display = toggleReply ? "inline-flex" : "none");
$$(".u3d-action-btn--dm, .u3d-action-btn--gift").forEach(el => el.style.display = toggleMessageGift ? "inline-flex" : "none");
document.documentElement.classList.toggle("u3d-action-fancy", !!loadSetting(K.FANCY_ACTIONS));
document.documentElement.classList.toggle("u3d-actions-hover", !!loadSetting(K.ACTIONS_HOVER));
applyThemeFromSettings();
applyUserCssIfEnabled();
const autoDelToggleEl = document.getElementById("autoDeleteToggle");
if (autoDelToggleEl) autoDelToggleEl.checked = autoDeleteEnabled;
const autoDelIntervalEl = document.getElementById("autoDeleteInterval");
if (autoDelIntervalEl) autoDelIntervalEl.value = intervalSec;
if (autoDeleteEnabled) startAutoDeleteTimer();
else stopAutoDeleteTimer();
}
function getEditorThemeFromInputs() {
const t = (id, def) => (document.getElementById(id)?.value || def);
return {
bg: t("tBg", "#0f172a"),
surface: t("tSurface", "#1e2738"),
surface2: t("tSurface2", "#2a3550"),
border: t("tBorder", "#3f495f"),
text: t("tText", "#eaeaea"),
muted: t("tMuted", "#a7b0c4"),
link: t("tLink", "#8cc0ff"),
linkHover: t("tLinkHover", "#cfe3ff"),
reply1: t("tReply1", "#2dd4bf"),
reply2: t("tReply2", "#14b8a6"),
dm1: t("tDm1", "#60a5fa"),
dm2: t("tDm2", "#3b82f6"),
gift1: t("tGift1", "#f59e0b"),
gift2: t("tGift2", "#ec4899"),
radius: Math.max(4, Math.min(20, parseInt(t("tRadius", "8"), 10) || 8))
};
}
function initThemesUI() {
const active = String(loadSetting(K.THEME_ACTIVE) || "default");
const radio = document.querySelector(`input[name="themePreset"][value="${active}"]`);
if (radio) radio.checked = true;
const customEditor = document.getElementById("themeCustomEditor");
if (customEditor) {
const isCustom = active === "custom";
customEditor.style.display = isCustom ? "block" : "none";
if (isCustom) {
try {
const obj = JSON.parse(loadSetting(K.THEME_CUSTOM) || "{}");
const set = (id, val) => {
const el = document.getElementById(id);
if (el && val !== undefined) el.value = val;
};
if (obj) {
set("tBg", obj.bg);
set("tSurface", obj.surface);
set("tSurface2", obj.surface2);
set("tBorder", obj.border);
set("tText", obj.text);
set("tMuted", obj.muted);
set("tLink", obj.link);
set("tLinkHover", obj.linkHover);
set("tReply1", obj.reply1);
set("tReply2", obj.reply2);
set("tDm1", obj.dm1);
set("tDm2", obj.dm2);
set("tGift1", obj.gift1);
set("tGift2", obj.gift2);
const r = document.getElementById("tRadius");
if (r) r.value = obj.radius || 8;
}
}
catch {}
}
}
const cssEnabledEl = document.getElementById("userCssEnable");
const cssTextEl = document.getElementById("userCssText");
if (cssEnabledEl) cssEnabledEl.checked = !!loadSetting(K.USERCSS_ENABLED);
if (cssTextEl) cssTextEl.value = String(loadSetting(K.USERCSS_CODE) || "");
}
function setupSettingsListeners() {
const mapSwitch = (id, key) => {
const el = document.getElementById(id);
if (el) el.addEventListener("change", e => {
saveSetting(key, e.target.checked);
applySettings();
});
};
const mapInput = (id, key, normalize) => {
const el = document.getElementById(id);
if (el) el.addEventListener("change", e => {
const val = normalize ? normalize(e.target.value) : e.target.value;
saveSetting(key, val);
applySettings();
});
};
mapSwitch("toggleBBCodes", K.TOGGLE_BBCODES);
mapSwitch("toggleMessageGift", K.TOGGLE_MSG_GIFT);
mapSwitch("toggleReply", K.TOGGLE_REPLY);
mapSwitch("togglePanel", K.TOGGLE_PANEL);
mapSwitch("fancyActions", K.FANCY_ACTIONS);
mapSwitch("actionsHover", K.ACTIONS_HOVER);
const autoDelToggle = document.getElementById("autoDeleteToggle");
if (autoDelToggle) autoDelToggle.addEventListener("change", (e) => {
saveSetting(K.AUTO_DELETE_ENABLED, e.target.checked);
applySettings();
});
mapInput("autoDeleteInterval", K.AUTO_DELETE_INTERVAL, v => Math.max(MIN_INTERVAL_SEC, toInt(v, 5)));
// Event delegation for all Settings buttons/toggles inside the panel
const panel = document.getElementById(settingsPanelID);
if (!panel) return;
panel.addEventListener("click", async (e) => {
const btn = e.target.closest("button");
if (!btn) return;
switch (btn.id) {
case "openThemesBtn":
initThemesUI();
showSettingsView("themes");
break;
case "openChangelogBtn":
renderChangelog();
showSettingsView("changelog");
break;
case "backFromThemesBtn":
case "backFromChangelogBtn":
showSettingsView("home");
break;
// Theme editor
case "themeApplyBtn": {
const obj = getEditorThemeFromInputs();
applyThemeFromObj(obj); // preview only
break;
}
case "themeSaveBtn": {
const obj = getEditorThemeFromInputs();
saveSetting(K.THEME_CUSTOM, JSON.stringify(obj));
saveSetting(K.THEME_ACTIVE, "custom");
applySettings(); // apply + persist
initThemesUI(); // update radios/editor state
break;
}
case "themeResetBtn": {
saveSetting(K.THEME_ACTIVE, "default");
applySettings();
initThemesUI();
break;
}
// Custom CSS
case "userCssApplyBtn": {
const css = $("#userCssText")?.value || "";
ensureUserCssStyleEl().textContent = css;
break;
}
case "userCssSaveBtn": {
const css = $("#userCssText")?.value || "";
saveSetting(K.USERCSS_CODE, css);
applyUserCssIfEnabled();
break;
}
case "userCssDefaultBtn": {
const def = String(DEFAULTS[K.USERCSS_CODE] || "");
const ta = $("#userCssText");
if (ta) ta.value = def;
break;
}
case "userCssExportBtn": {
const t = getActiveThemeObject();
const rootCss = `:root{
--u3d-bg:${t.bg}; --u3d-surface:${t.surface}; --u3d-surface2:${t.surface2}; --u3d-border:${t.border};
--u3d-text:${t.text}; --u3d-muted:${t.muted}; --u3d-link:${t.link}; --u3d-link-hover:${t.linkHover};
--u3d-reply-1:${t.reply1}; --u3d-reply-2:${t.reply2};
--u3d-dm-1:${t.dm1}; --u3d-dm-2:${t.dm2};
--u3d-gift-1:${t.gift1}; --u3d-gift-2:${t.gift2};
--u3d-action-radius:${t.radius}px;
}
/* Custom CSS */\n`;
const custom = String(loadSetting(K.USERCSS_CODE) || "");
const bundle = rootCss + custom;
try {
await navigator.clipboard.writeText(bundle);
alert("Theme CSS copied to clipboard.");
}
catch {
prompt("Copy your CSS:", bundle);
}
break;
}
case "userCssImportBtn": {
try {
const clip = await navigator.clipboard.readText();
if (clip) $("#userCssText").value = clip;
else {
const manual = prompt("Paste CSS here:", "");
if (manual !== null) $("#userCssText").value = manual;
}
}
catch {
const manual = prompt("Paste CSS here:", "");
if (manual !== null) $("#userCssText").value = manual;
}
break;
}
case "userCssClearBtn": {
const ta = $("#userCssText");
if (ta) ta.value = "";
ensureUserCssStyleEl().textContent = "";
saveSetting(K.USERCSS_CODE, "");
break;
}
}
});
// Delegated changes (theme preset radios and CSS enable switch)
panel.addEventListener("change", (e) => {
const t = e.target;
if (!t) return;
if (t.name === "themePreset") {
saveSetting(K.THEME_ACTIVE, t.value);
applySettings(); // apply theme
initThemesUI(); // show/hide editor
}
else if (t.id === "userCssEnable") {
saveSetting(K.USERCSS_ENABLED, t.checked);
applyUserCssIfEnabled();
}
});
}
function setupChatFeatures(chatbox) {
if (!document.getElementById(bbCodesPanelID)) {
const shell = document.createElement("div");
shell.id = "bbCodesPanelShell";
chatbox.parentNode.insertBefore(shell, chatbox.nextSibling);
const holder = document.createElement("div");
holder.innerHTML = BBCODES_PANEL_HTML;
shell.appendChild(holder);
const moreBtn = holder.querySelector("#bbCodeDropdown");
const menu = holder.querySelector("#bbCodeDropdownMenu");
if (moreBtn && menu) {
const toggleMenu = (force) => {
const wantOpen = typeof force === "boolean" ? force : !menu.classList.contains("open");
menu.classList.toggle("open", wantOpen);
moreBtn.classList.toggle("open", wantOpen);
};
moreBtn.addEventListener("click", (e) => {
e.stopPropagation();
toggleMenu();
});
menu.addEventListener("click", (e) => e.stopPropagation());
document.addEventListener("click", () => toggleMenu(false));
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") toggleMenu(false);
});
}
holder.querySelectorAll("[data-bbcode],[data-action]").forEach(span => {
span.addEventListener("click", (e) => {
e.stopPropagation();
const bbCode = span.getAttribute("data-bbcode");
const action = span.getAttribute("data-action");
if (bbCode) {
if (bbCode === "[img][/img]") insertImgBBCodeWithClipboard(bbCode, chatbox);
else if (bbCode === "[url][/url]") insertBBCodeWithClipboard(bbCode, chatbox);
else if (bbCode === "[youtube][/youtube]") insertYouTubeBBCodeSmart(bbCode, chatbox);
else if (bbCode === "/msg" || bbCode === "/gift") {
chatbox.setRangeText(bbCode + " ", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
}
else if (bbCode === "[hr]") chatbox.setRangeText("[hr]\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
else insertBBCode(chatbox, bbCode);
}
else if (action) {
switch (action) {
case "color":
insertWrapWithArg(chatbox, "color", "Color (name or #rrggbb):", "#ff0000");
break;
case "size":
insertWrapWithArg(chatbox, "size", "Text size (e.g., 12, 14, 18):", "14");
break;
case "list-ul":
chatbox.setRangeText("[list]\n[*] Item 1\n[*] Item 2\n[/list]\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
break;
case "list-ol":
chatbox.setRangeText("[list=1]\n[*] Item 1\n[*] Item 2\n[/list]\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
break;
case "list-item":
chatbox.setRangeText("[*] Item\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
break;
case "hr":
chatbox.setRangeText("[hr]\n", chatbox.selectionStart, chatbox.selectionEnd, "end");
chatbox.focus();
break;
}
}
});
});
}
}
function setupSettingsPanel() {
const header = gChat.header || document.getElementById("chatbox_header");
if (!header) return console.error("Failed to attach the settings button: chat header not found.");
if (document.getElementById("u3dSettingsAnchor")) {
applySettings();
setupSettingsListeners();
return;
}
const anchor = document.createElement("div");
anchor.className = "u3d-settings-anchor";
anchor.id = "u3dSettingsAnchor";
anchor.innerHTML = `<div id="${settingsButtonID}"><i class="fa-solid fa-gear"></i></div>${settingsPanelHTML()}`;
header.appendChild(anchor);
const toggleSettingsButton = document.getElementById(settingsButtonID);
const settingsPanelElement = document.getElementById(settingsPanelID);
toggleSettingsButton.addEventListener("click", (event) => {
event.stopPropagation();
settingsPanelElement.style.display = (settingsPanelElement.style.display === "block") ? "none" : "block";
});
document.addEventListener("click", (event) => {
if (!settingsPanelElement.contains(event.target) && !toggleSettingsButton.contains(event.target)) {
settingsPanelElement.style.display = "none";
}
});
initThemesUI();
showSettingsView("home");
applySettings();
setupSettingsListeners();
}
async function checkAndSetup() {
try {
gChat.box = await waitForAny(SELECTORS.chatbox);
gChat.messages = await waitForAny(SELECTORS.messages);
gChat.header = await waitForAny(SELECTORS.header).catch(() => null);
if (gChat.header) gChat.header.classList.add("u3d-chat-header");
setupReplyFeatures();
setupChatFeatures(gChat.box);
setupSettingsPanel();
applySettings();
}
catch (error) {
console.error("Error setting up chat UI:", error);
}
}
checkAndSetup();
})();
}