// ==UserScript== // @name Better MWI Chat // @namespace http://tampermonkey.net/ // @version 1.2.1 // @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-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{ display: inline-block; } button.repeat-msg-button:hover{ cursor: pointer; } button.repeat-msg-button{ display: none; 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(` ic_fluent_mention_24_regular Created with Sketch. `); 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});