it would have actually been more useful to have a shortcut for it, but the terminal to bbcode thing is just a sidebar button. the chatbox button i have is [P], for Pomf, because it's what i used to use before szyup before eeprom
i am here once again with a usage survey
Does anyone use the umi:message_add
event in general, or the message
field of the umi:ui:message_add
event at all?
If so, can you modify it to just use the element
field of umi:ui:message_add
instead? (please do not use the dataset fields and expect them to exist in the future, they are subject to change)
If not, what data from the message object are you using?
i dont know if there's a better way to do this, but ui for deleting message
// ==UserScript==
// @name Flashii Delete Message Button
// @namespace http://no.com
// @version 1.3
// @description Adds a delete button to each of your own message in Flashii Chat
// @author no
// @match *://chat.flashii.net/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Ensures chat is loaded
window.addEventListener('umi:connect', function(ev) {
// Function to add delete button to your own messages
function addDeleteButtons() {
const userId = Umi.User.getCurrentUser().id;
const messages = document.querySelectorAll('.message');
messages.forEach(message => {
const messageId = message.getAttribute('data-id');
const messageAuthor = message.getAttribute('data-author');
const messageBody = message.getAttribute('data-body');
// Exclude messages containing join and leave phrases, and with the class message-tiny
const excludedPhrases = ["has disconnected", "has joined"];
const containsExcludedPhrase = excludedPhrases.some(phrase => messageBody.includes(phrase));
const isTinyMessage = message.classList.contains('message-tiny');
if ((messageAuthor === userId && !containsExcludedPhrase && !message.querySelector('.delete-button')) ||
(isTinyMessage && messageAuthor === userId && !containsExcludedPhrase && !message.querySelector('.delete-button'))) {
const deleteButton = document.createElement('button');
deleteButton.innerHTML = '×'; // Cross symbol
deleteButton.className = 'delete-button';
deleteButton.style.position = 'absolute';
deleteButton.style.right = '10px';
deleteButton.style.top = '50%';
deleteButton.style.transform = 'translateY(-50%)';
deleteButton.style.backgroundColor = '#333';
deleteButton.style.color = '#fff';
deleteButton.style.border = 'none';
deleteButton.style.padding = '2px 5px';
deleteButton.style.borderRadius = '1px';
deleteButton.style.cursor = 'pointer';
deleteButton.addEventListener('click', () => {
Umi.Server.SendMessage(`/delete ${messageId}`);
}
);
message.style.position = 'relative';
message.appendChild(deleteButton);
}
});
}
// Add delete buttons when the script is loaded
addDeleteButtons();
// Observe mutations to add delete buttons to new messages
const observer = new MutationObserver(addDeleteButtons);
observer.observe(document.body, { childList: true, subtree: true });
});
})();
---

nook remover
// ==UserScript==
// @name Nook Remover Reimagined
// @namespace https://saikuru.net/
// @version 2024-11-24
// @description Automatically removes embeds from nook
// @author You
// @match https://chat.flashii.net/
// @icon https://www.google.com/s2/favicons?sz=64&domain=flashii.net
// @grant none
// ==/UserScript==
(function () {
'use strict';
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.matches && element.matches('div.message--user-181')) {
element.querySelectorAll('embed, iframe, object, img, video, audio').forEach((embeddable) => {
embeddable.remove();
});
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();

// ==UserScript==
// @name Flashii Chat - Better Quotes & Delete Button
// @version 2.1
// @description Adds additional behaviour to quote selected text in messages, adds buttons to quote whole messages, and delete your own messages.
// @author lester
// @match *://chat.flashii.net/*
// @grant none
// ==/UserScript==
(() => {
let selectedText = '';
const cssID = 'chat-style';
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) {
input.value = input.value.trim() === '[quote][/quote]' ? `[quote]${selectedText}[/quote]` : input.value + `[quote]${selectedText}[/quote]`;
input.focus();
selectedText = '';
}
}, 50);
}
});
window.addEventListener('umi:connect', () => {
const uid = Umi.User.getCurrentUser().id;
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 injectCSS = () => {
if (!document.getElementById(cssID)) {
const s = document.createElement('style');
s.id = cssID;
s.textContent = `
.message .quote-button, .message .delete-button {
opacity: 0;
position: absolute;
top: 50%;
transform: translateY(-50%);
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 {
background: var(--theme-colour-input-menu-button-hover);
}
.message .quote-button:active, .message .delete-button:active {
background: var(--theme-colour-input-menu-button-active);
}
.message:hover .quote-button, .message:hover .delete-button {
opacity: 1;
}
`;
document.head.appendChild(s);
}
};
const addButtons = () => {
injectCSS();
const input = document.querySelector('textarea.input__text');
document.querySelectorAll('.message').forEach(msg => {
const id = msg.dataset.id, author = msg.dataset.author, body = msg.dataset.body || '';
if (!input || ['has disconnected', 'has joined'].some(p => body.includes(p))) return;
const hasQuote = msg.querySelector('.quote-button');
const hasDelete = msg.querySelector('.delete-button');
const textEl = msg.querySelector('.message__text') || msg.querySelector('.message-tiny-text');
const userEl = msg.querySelector('.message__user');
const timeEl = msg.querySelector('.message__time');
if (!hasQuote && textEl && userEl && timeEl) {
const btn = document.createElement('button');
btn.className = 'quote-button';
btn.textContent = 'Quote';
btn.style.right = '10px';
btn.title = 'Quote this message';
btn.onclick = () => {
const name = userEl.textContent.trim() || 'Unknown';
const time = timeEl.textContent.trim();
const msgText = textEl.textContent.trim();
const raw = userEl.style.color;
const userColor = (!raw || raw === 'inherit') ? '#ffffff' : rgbToHex(raw);
input.value += `[color=${userColor}][b]${name}[/b][/color][color=#c0c0c0] @ ${time} — [/color][quote]${msgText}[/quote]`;
input.focus();
};
msg.style.position = 'relative';
msg.appendChild(btn);
}
if (!hasDelete && author === uid && (textEl || msg.classList.contains('message-tiny'))) {
const del = document.createElement('button');
del.className = 'delete-button';
del.innerHTML = '×';
del.style.right = '60px';
del.title = 'Delete this message';
del.onclick = () => Umi.Server.SendMessage(`/delete ${id}`);
msg.style.position = 'relative';
msg.appendChild(del);
}
});
};
addButtons();
new MutationObserver(addButtons).observe(document.body, { childList: true, subtree: true });
});
})();
---


// ==UserScript==
// @name Flashii Chat - File Upload Progress Bar
// @version 1.1
// @description Show progress bar beside Spoiler button during file upload
// @author lester
// @match *://chat.flashii.net/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const originalXHR = window.XMLHttpRequest;
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 updateProgress(percent) {
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 (percent >= 100) {
setTimeout(() => {
wrapper.style.opacity = '0';
setTimeout(() => {
inner.style.width = '0%';
inner.textContent = '';
}, 150);
}, 800);
}
}
function CustomXHR() {
const xhr = new originalXHR();
xhr.open = function (method, url) {
this._isUpload = method === 'POST' && url.includes('/uploads');
return originalXHR.prototype.open.apply(this, arguments);
};
xhr.send = function (body) {
if (this._isUpload) {
createProgressBar();
this.upload.onprogress = e => {
if (e.lengthComputable) updateProgress(Math.round((e.loaded / e.total) * 100));
};
}
return originalXHR.prototype.send.apply(this, arguments);
};
return xhr;
}
window.XMLHttpRequest = CustomXHR;
})();
---

some improvements to the previous quote thingy i made
// ==UserScript==
// @name Flashii Chat - Better Quotes & Delete Button
// @version 3.0
// @description Adds message quoting and preview, via button and timestamp. Adds delete button to own messages.
// @author lester
// @match *://chat.flashii.net/*
// @grant none
// ==/UserScript==
(() => {
let selectedText = '';
let pendingQuote = null;
let previewInterval = null;
const cssID = 'chat-enhanced-style';
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;
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) => {
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-input-border);
padding: 6px 10px;
font-size: 13px;
margin: 4px 0;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
#quote-preview span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#cancel-quote {
background: none;
border: none;
color: var(--theme-colour-main-colour);
cursor: pointer;
font-weight: bold;
padding: 0 6px;
font-size: 13px;
}
.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;
}
`;
document.head.appendChild(style);
};
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);
};
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 updateTime = () => {
const time = getRelativeTime(created);
span.innerHTML = `<span>Quoting </span><i><b style="color: ${color};">${name}</b> @ ${time} </i> "${msg.slice(0, 100)}..."`;
};
updateTime();
previewInterval = setInterval(updateTime, 1000);
};
const addButtons = () => {
injectCSS();
const input = document.querySelector('textarea.input__text');
const form = document.querySelector('form.input');
document.querySelectorAll('.message').forEach(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 = msg.dataset.body || textEl?.textContent.trim() || '';
const endQuoteIdx = msgText.lastIndexOf('[/quote]');
if (endQuoteIdx !== -1) msgText = msgText.slice(endQuoteIdx + 8).trim();
msgText = msgText.replace(/\[Embed\]|\[Remove\]/g, '').trim();
if (timeEl) {
timeEl.onclick = () => {
pendingQuote = { name, color: userColor, created: dataCreated, id, msg: msgText };
showQuotePreview(pendingQuote);
input.focus();
};
}
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}`);
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 = () => {
pendingQuote = { name, color: userColor, created: dataCreated, id, msg: msgText };
showQuotePreview(pendingQuote);
input.focus();
};
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);
}
};
btnContainer.appendChild(go);
}
}
});
if (form && !form.__quoteIntercepted) {
form.__quoteIntercepted = true;
form.addEventListener('submit', e => {
if (pendingQuote && input) {
const { name, color, id, msg, created } = pendingQuote;
const hidden = '\u200C';
const cleanMsg = msg.replace(/\[Embed\]|\[Remove\]/g, '').trim();
const time = getRelativeTime(created);
const quoteBlock = `[i]${color ? `[color=${color}]` : ''}[b]${name}[/b]${color ? '[/color]' : ''} @ ${time}[/i][url=#${id}]${hidden}[/url] [quote]${cleanMsg}[/quote]`;
input.value = `${quoteBlock}${input.value ? '\n' + input.value.trimStart() : ''}`;
pendingQuote = null;
const preview = document.getElementById('quote-preview');
if (preview) preview.remove();
clearInterval(previewInterval);
}
}, true);
}
};
addButtons();
new MutationObserver(addButtons).observe(document.body, { childList: true, subtree: true });
});
})();
---

// ==UserScript==
// @name Flashii Chat - Better Quotes & Delete (via timestamp only)
// @version 3.0
// @description Adds message quoting and preview, via timestamp. Adds delete button to own messages.
// @author lester
// @match *://chat.flashii.net/*
// @grant none
// ==/UserScript==
(() => {
let selectedText = '';
let pendingQuote = null;
let previewInterval = null;
const cssID = 'chat-enhanced-style';
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;
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) => {
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 .delete-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 .delete-button:hover {
background: var(--theme-colour-input-menu-button-hover);
}
.message .delete-button:active {
background: var(--theme-colour-input-menu-button-active);
}
.message:hover .delete-button {
opacity: 1;
}
#quote-preview {
background: var(--theme-colour-input-background);
border: 1px solid var(--theme-colour-input-border);
padding: 6px 10px;
font-size: 13px;
margin: 4px 0;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
#quote-preview span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#cancel-quote {
background: none;
border: none;
color: var(--theme-colour-main-colour);
cursor: pointer;
font-weight: bold;
padding: 0 6px;
font-size: 13px;
}
.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;
}
`;
document.head.appendChild(style);
};
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);
};
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 updateTime = () => {
const time = getRelativeTime(created);
span.innerHTML = `<span>Quoting </span><i><b style="color: ${color};">${name}</b> @ ${time} </i> "${msg.slice(0, 100)}..."`;
};
updateTime();
previewInterval = setInterval(updateTime, 1000);
};
const addButtons = () => {
injectCSS();
const input = document.querySelector('textarea.input__text');
const form = document.querySelector('form.input');
document.querySelectorAll('.message').forEach(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 = msg.dataset.body || textEl?.textContent.trim() || '';
const endQuoteIdx = msgText.lastIndexOf('[/quote]');
if (endQuoteIdx !== -1) msgText = msgText.slice(endQuoteIdx + 8).trim();
msgText = msgText.replace(/\[Embed\]|\[Remove\]/g, '').trim();
if (timeEl) {
timeEl.onclick = () => {
pendingQuote = { name, color: userColor, created: dataCreated, id, msg: msgText };
showQuotePreview(pendingQuote);
input.focus();
};
}
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}`);
btnContainer.appendChild(del);
}
});
if (form && !form.__quoteIntercepted) {
form.__quoteIntercepted = true;
form.addEventListener('submit', e => {
if (pendingQuote && input) {
const { name, color, id, msg, created } = pendingQuote;
const hidden = '\u200C';
const cleanMsg = msg.replace(/\[Embed\]|\[Remove\]/g, '').trim();
const time = getRelativeTime(created);
const quoteBlock = `[i]${color ? `[color=${color}]` : ''}[b]${name}[/b]${color ? '[/color]' : ''} @ ${time}[/i][url=#${id}]${hidden}[/url] [quote]${cleanMsg}[/quote]`;
input.value = `${quoteBlock}${input.value ? '\n' + input.value.trimStart() : ''}`;
pendingQuote = null;
const preview = document.getElementById('quote-preview');
if (preview) preview.remove();
clearInterval(previewInterval);
}
}, true);
}
};
addButtons();
new MutationObserver(addButtons).observe(document.body, { childList: true, subtree: true });
});
})();
---
