// ==UserScript==
// @name Better MWI Chat
// @namespace http://tampermonkey.net/
// @version 1.2.2
// @description Make Chat Great Again!
// @author VoltaX
// @match https://www.milkywayidle.com/*
// @icon http://milkywayidle.com/favicon.ico
// @grant none
// @downloadURL none
// ==/UserScript==
const css =
`
.mwibetterchat-invisible{
opacity: 0;
}
.mwibetterchat-disable{
display: none;
opacity: 0;
}
.rotate-left{
transform: rotate(90deg);
}
.rotate-right{
transform: rotate(-90deg);
}
div.ChatMessage_chatMessage__2wev4[processed]:not([not-modified]){
display: flex;
flex-direction: column;
}
div.chat-message-header{
display: flex;
flex-direction: row;
}
div.chat-message-header span.timespan{
display: none;
}
div.chat-message-header:hover span.timespan{
display: auto;
}
input.Chat_chatInput__16dhX{
width: 100%;
}
div.chat-message-body{
border-radius: 10px;
margin: 3px;
background: var(--color-space-600);
padding: 5px 8px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
flex-shrink: 1;
width: fit-content;
}
div.chat-message-line{
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: fit-content;
}
img.chat-image{
margin: 3px 0px;
display: block;
max-width: 100%;
height: auto;
}
div.chat-message-body-wrapper{
display: flex;
flex-direction: row;
flex-wrap: no-wrap;
align-items: end;
}
button.repeat-msg-button:hover, div.chat-message-body-wrapper:hover button.repeat-msg-button, div.ChatMessage_chatMessage__2wev4:hover button.repeat-msg-button{
opacity: 1;
}
button.repeat-msg-button:hover{
cursor: pointer;
}
button.repeat-msg-button{
display: inline-block;
opacity: 0;
margin: 3px 3px 6px;
padding: 0px -2px;
width: 24px;
height: 24px;
line-height: 16px;
font-size: 10px;
text-wrap: nowrap;
border-radius: 12px;
--repeat-button-color: var(--color-ocean-250);
border: 2px solid var(--repeat-button-color);
color: var(--repeat-button-color);
background: rgba(0, 0, 0, 0);
}
div.input-wrapper{
flex-grow: 1;
}
button.input-clear-button{
position: absolute;
right: 62px;
top: 4px;
background: none;
border: none;
}
button.input-clear-button:hover{
cursor: pointer;
}
button.scroll-to-bottom{
position: absolute;
height: 40px;
width: 40px;
border-radius: 20px;
border: none;
background: var(--color-market-buy);
bottom: 40px;
right: 10px;
opacity: 1;
@starting-style{
opacity: 0;
}
transition: 0.3s ease allow-discrete;
}
button.scroll-to-bottom:hover{
cursor: pointer;
background: var(--color-market-buy-hover);
opacity: 1;
}
`;
const html = (html) => {
const t = document.createElement("template");
t.innerHTML = html;
return t.content.firstElementChild;
};
const svg_cross = html(``);
const svg_mention = html(``);
const svg_arrow_head = html(``)
const InsertStyleSheet = (style) => {
const s = new CSSStyleSheet();
s.replaceSync(style);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, s];
};
InsertStyleSheet(css);
const HTML = (tagname, attrs, ...children) => {
if(attrs === undefined) return document.createTextNode(tagname);
const ele = document.createElement(tagname);
let outputFlag = false;
let outputTarget = null;
if(attrs) for(const [key, value] of Object.entries(attrs)){
if(value === null || value === undefined) continue;
if(key.charAt(0) === "_"){
const type = key.slice(1);
ele.addEventListener(type, value);
}
if(key.charAt(0) === "!"){
outputFlag = key.slice(1);
if(typeof(value) === "object") outputTarget = value;
}
else if(key === "eventListener"){
for(const listener of value){
ele.addEventListener(listener.type, listener.listener, listener.options);
}
}
else ele.setAttribute(key, value);
}
for(const child of children) if(child) ele.append(child);
if(outputFlag && outputTarget) outputTarget[outputFlag] = ele;
return ele;
};
const ProcessChatMessage = () => {
document.querySelectorAll("div.ChatHistory_chatHistory__1EiG3 > div.ChatMessage_chatMessage__2wev4:not([processed])").forEach(div => {
div.setAttribute("processed", "");
const timeSpan = div.children[0];
const parent = div.parentElement;
const isLastChild = parent.querySelector(":scope > div:nth-last-child(1)") === div;
timeSpan.classList.add("timespan");
const nameSpan = div.querySelector(":scope span.ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2")?.parentElement?.parentElement?.parentElement;
if(!nameSpan) {
div.setAttribute("not-modified", "");
return;
}
const nameWrapper = HTML("div", {class: "chat-message-header"});
nameWrapper.replaceChildren(nameSpan, timeSpan);
const bubble = HTML("div", {class: "chat-message-body-wrapper"});
const contentWrapper = HTML("div", {class: "chat-message-body"});
contentWrapper.replaceChildren(...[...div.children].reduce(({newLine, lines}, ele) => {
if(ele.tagName === "A" && ele.type?.includes("image/") || /\.(?:apng|avif|bmp|gif|ico|jpeg|jpg|png|tif|tiff|webp)$/.test(ele.href)){
lines.push(HTML("div", {class: "chat-message-line"},
HTML("img", {class: "chat-image", src: ele.href})
));
newLine = true;
}
else if(newLine) lines.push(HTML("div", {class: "chat-message-line"}, ele));
else lines.at(-1).append(ele);
return {newLine, lines};
}, {newLine: false, lines: [HTML("div", {class: "chat-message-line"})]}).lines);
const repeat = HTML("button", {class: "repeat-msg-button", _click: () => {
const contentBuilder = [];
[...contentWrapper.children].flatMap(line => [...line.children]).forEach(ele => {
if(ele.tagName === "SPAN") contentBuilder.push(ele.innerText);
if(ele.tagName === "A") contentBuilder.push(ele.getAttribute("href"));
if(ele.tagName === "DIV" && ele.classList.contains("ChatMessage_linkContainer__18Kv3")){
const svg = ele.querySelector(':scope svg[aria-label="Skill"]');
if(svg) contentBuilder.push(`[${svg.children[0].getAttribute("href").split("#").at(-1)}]`);
}
});
console.log(contentBuilder);
const input = document.querySelector("input.Chat_chatInput__16dhX");
const prevVal = input.value;
input.value = contentBuilder.join("");
const ev = new Event("input", {bubbles: true});
ev.simulated = true;
const tracker = input._valueTracker;
if(tracker) tracker.setValue(prevVal);
input.dispatchEvent(ev);
}}, "+1");
bubble.replaceChildren(contentWrapper, repeat);
div.replaceChildren(nameWrapper, bubble);
const contentHeight = div.getBoundingClientRect().height;
if(isLastChild && (parent.offsetHeight + parent.scrollTop + contentHeight > parent.scrollHeight)) parent.scrollTop = parent.scrollTop + contentHeight;
/*
const texts = [...div.querySelectorAll(`:scope > span:not([style="display: inline-block;"]):not(.ChatMessage_timestamp__1iRZO)`)];
texts.forEach(text => text.textContent = text.textContent.replaceAll("喵", ""));
const userName = div.querySelector(":scope div.CharacterName_name__1amXp")?.dataset?.name;
if(!userName) return;
const content = texts.map(span => span.textContent).join("");
if(content.length > 0){
const DoRepeat = () => {
const input = document.querySelector("input.Chat_chatInput__16dhX");
const prevVal = input.value;
input.value = content;
const ev = new Event("input", {bubbles: true});
ev.simulated = true;
const tracker = input._valueTracker;
if(tracker) tracker.setValue(prevVal);
input.dispatchEvent(ev);
}
div.insertAdjacentElement("beforeend",
HTML("button", {class: "comment-improvement-button repeat-comment-button", _click: DoRepeat}, " + 1 ")
);
texts.forEach(text => text.addEventListener("click", DoRepeat));
}
const DoMentionOrWhisper = (isMention) => () => {
const mentionStr = `@${userName}`;
const input = document.querySelector("input.Chat_chatInput__16dhX");
const prevVal = input.value;
input.value = isMention ? `${mentionStr} ${prevVal.replaceAll(/@[a-zA-Z0-9]+/g, "")}` : `/w ${userName} ${prevVal.replaceAll(/\/w [a-zA-Z0-9]+/g, "")}`;
const ev = new Event("input", {bubbles: true});
ev.simulated = true;
const tracker = input._valueTracker;
if(tracker) tracker.setValue(prevVal);
input.dispatchEvent(ev);
};
div.insertAdjacentElement("beforeend",
HTML("button", {class: "comment-improvement-button mention-sender-button", _click: DoMentionOrWhisper(true)}, "@此人")
);
div.insertAdjacentElement("beforeend",
HTML("button", {class: "comment-improvement-button whisper-button", _click: DoMentionOrWhisper(false)}, "私聊")
);
*/
})
};
const AddSwitchButton = (chatDiv) => {
const collapse = chatDiv.querySelector(":scope div.TabsComponent_expandCollapseButton__6nOWk");
const collapsedupe = collapse.cloneNode(true);
const arrowSVG = collapsedupe.children[0];
arrowSVG.classList.add("rotate-left");
collapsedupe.addEventListener("click", () => {
collapse.classList.toggle("mwibetterchat-disable");
arrowSVG.classList.toggle("rotate-left");
arrowSVG.classList.toggle("rotate-right");
MoveChatPannel(false);
})
collapse.classList.add("mwibetterchat-disable");
collapse.insertAdjacentElement("afterend", collapsedupe);
};
const AddToBottomButton = (chatDiv) => {
const temp = {};
chatDiv.insertAdjacentElement("beforeend",
HTML("button", {class: "scroll-to-bottom", "!retVal": temp, _click: () => {
const chat = document.querySelector("div.TabPanel_tabPanel__tXMJF:not(.TabPanel_hidden__26UM3) div.ChatHistory_chatHistory__1EiG3");
console.log(chat);
if(!chat) return;
chat.scrollTop = 99999;
}}, svg_arrow_head.cloneNode(true))
);
document.querySelectorAll("div.ChatHistory_chatHistory__1EiG3:not([listening])").forEach(div => {
div.setAttribute("listening", "");
div.addEventListener("scroll", (ev) => {
const t = ev.target;
const atBottom = t.offsetHeight + t.scrollTop + 25 > t.scrollHeight;
if(atBottom) temp.retVal.classList.add("mwibetterchat-disable");
else temp.retVal.classList.remove("mwibetterchat-disable");
})
});
}
const MoveChatPannel = (firstInvoked = true) => {
const chatDiv = document.querySelector(`div.Chat_chat__3DQkj${firstInvoked?":not([moved])":""}`);
const characterDiv = document.querySelector(`div.CharacterManagement_characterManagement__2PhvW${firstInvoked?":not([moved])":""}`);
if(!chatDiv || !characterDiv) return;
if(firstInvoked){
AddSwitchButton(chatDiv);
AddToBottomButton(chatDiv);
}
chatDiv.setAttribute("moved", "");
characterDiv.setAttribute("moved", "");
const chatWrapper = chatDiv.parentElement;
characterDiv.replaceWith(chatDiv);
chatWrapper.replaceChildren(characterDiv);
};
const ModifyChatInput = () => {
const input = document.querySelector("input.Chat_chatInput__16dhX:not([clear-button-added])");
if(!input) return;
input.setAttribute("clear-button-added", "");
const wrapper = HTML("div", {class: "input-wrapper"});
const clearBtn = HTML("button", {class: "input-clear-button", _click: () => {
const prevVal = input.value;
input.value = ""
const ev = new Event("input", {bubbles: true});
ev.simulated = true;
const tracker = input._valueTracker;
if(tracker) tracker.setValue(prevVal);
input.dispatchEvent(ev);
}}, svg_cross.cloneNode(true));
input.replaceWith(wrapper);
wrapper.replaceChildren(input, clearBtn);
};
const OnMutate = (mutlist, observer) => {
observer.disconnect();
MoveChatPannel();
ModifyChatInput();
ProcessChatMessage();
observer.observe(document, {subtree: true, childList: true});
};
new MutationObserver(OnMutate).observe(document, {subtree: true, childList: true});