// ==UserScript== // @name Better MWI Chat // @namespace http://tampermonkey.net/ // @version 1.3.3 // @description Make Chat Great Again! // @author VoltaX // @match https://www.milkywayidle.com/* // @icon http://milkywayidle.com/favicon.ico // @grant none // @downloadURL https://update.greasyfork.icu/scripts/535848/Better%20MWI%20Chat.user.js // @updateURL https://update.greasyfork.icu/scripts/535848/Better%20MWI%20Chat.meta.js // ==/UserScript== let Setting = { }; //let playerUsername = ""; const LoadSetting = () => { try{ Setting = {...Setting, ...JSON.parse(window.localStorage.getItem("better-chat-settings") ?? "{}")}; } catch(e){ console.error(e); } }; const SaveSetting = () => window.localStorage.setItem("better-chat-settings", JSON.stringify(Setting)); 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{ padding: 0px 3px; 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 6px; display: flex; flex-direction: column; flex-wrap: wrap; flex-shrink: 1; width: fit-content; } div.chat-message-body.mentioned{ background: var(--color-ocean-400); } 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; border-radius: 5px; } 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); } button.interact-user-button{ margin-left: 6px; margin-right: -4px; width: 24px; height: 20px; opacity: 0; border: 0px none; background: none; } div.ChatMessage_chatMessage__2wev4:hover button.interact-user-button{ opacity: 1; } button.interact-user-button:hover{ cursor: pointer; } 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_whisper = 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); } else 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 KeepAtBottom = (chatMsgDiv, always = false) => { const contentHeight = chatMsgDiv.getBoundingClientRect().height; const parent = chatMsgDiv.parentElement; const flag = always || parent.querySelector(":scope > div:nth-last-child(1)") === chatMsgDiv; if(flag && (parent.offsetHeight + parent.scrollTop + contentHeight > parent.scrollHeight)) parent.scrollTop = parent.scrollTop + contentHeight; }; 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 nameSpan = div.querySelector(":scope span.ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2")?.parentElement?.parentElement?.parentElement; if(!nameSpan) { div.setAttribute("not-modified", ""); return; } timeSpan.remove(); nameSpan.children[0].children[0].children[1].remove(); const username = nameSpan.querySelector(":scope .CharacterName_name__1amXp")?.dataset?.name ?? ""; const nameWrapper = HTML("div", {class: "chat-message-header"}); nameWrapper.replaceChildren(nameSpan); 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) => { // test if content mentions user //if(playerUsername && ele.tagName === "SPAN" && ele.innerText.includes(`@${playerUsername}`)) contentWrapper.classList.add("mentioned"); // replace img link to element 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, _load: () => KeepAtBottom(div, true)}) )); 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 repeatBtn = 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); else if(ele.tagName === "A") contentBuilder.push(ele.getAttribute("href")); else if(ele.tagName === "IMG") contentBuilder.push(ele.getAttribute("src")); else 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); input.focus(); }}, "+1"); if(username){ 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); }; const mentionBtn = HTML("button", {class: "interact-user-button", _click: DoMentionOrWhisper(true)}, svg_mention.cloneNode(true)); const whisperBtn = HTML("button", {class: "interact-user-button", _click: DoMentionOrWhisper(false)}, svg_whisper.cloneNode(true)); nameWrapper.append(mentionBtn, whisperBtn); } bubble.replaceChildren(contentWrapper, repeatBtn); div.replaceChildren(nameWrapper, bubble); KeepAtBottom(div); }) }; 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) => { chatDiv.insertAdjacentElement("beforeend", HTML("button", {class: "scroll-to-bottom", _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)) ); }; const AddToBottomButtonListeners = () => { const btn = document.querySelector("button.scroll-to-bottom"); if(!btn) return; 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) btn.classList.add("mwibetterchat-disable"); else btn.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 UpdateUsername = () => { if(!playerUsername){ const characterInfoDiv = document.querySelector("div.Header_characterInfo__3ixY8"); const username = characterInfoDiv?.querySelector(":scope div.CharacterName_name__1amXp")?.dataset?.name; if(username) playerUsername = username; } }; */ const OnMutate = (mutlist, observer) => { observer.disconnect(); //UpdateUsername(); MoveChatPannel(); ModifyChatInput(); AddToBottomButtonListeners(); ProcessChatMessage(); observer.observe(document, {subtree: true, childList: true}); }; if(window.matchMedia("only screen and (max-width: 1024px)").matches === false) new MutationObserver(OnMutate).observe(document, {subtree: true, childList: true});