Greasy Fork is available in English.

IdlePixel Chat Overhaul

Overhaul of chat UI and functionality.

// ==UserScript==
// @name         IdlePixel Chat Overhaul
// @namespace    com.anwinity.idlepixel
// @version      1.1.1
// @description  Overhaul of chat UI and functionality.
// @author       Wynaan
// @license      MIT
// @match        *://idle-pixel.com/login/play*
// @require      https://greasyfork.org/scripts/441206-idlepixel/code/IdlePixel+.js?anticache=20220905
// @grant none
// ==/UserScript==

(function() {
    'use strict';

    const CSS_VARIABLES_DEFAULTS = {
        "--chat-text-color":                "0, 0, 0",
        "--chat-username-color":            "70, 70, 70",
        "--chat-server-msg-tag-color":      "0, 82, 71",
        "--chat-server-msg-tag-bg-color":   "0, 214, 186",
        "--chat-server-msg-color":          "0, 0, 255",
        "--chat-timestamp-color":           "0, 128, 0",
        "--chat-level-color":               "128, 128, 128",
        "--chat-bg-color":                  "255, 255, 255",
        "--chat-outer-bg-color":            "166, 252, 255"
    };

    class ChatPlugin extends IdlePixelPlusPlugin {
        constructor() {
            super("chat_overhaul", {
                about: {
                    name: GM_info.script.name + " (ver: " + GM_info.script.version + ")",
                    version: GM_info.script.version,
                    author: GM_info.script.author,
                    description: GM_info.script.description
                },
                config: [
                    // {
                    //     id: "condensed",
                    //     label: "Condensed UI",
                    //     type: "boolean",
                    //     default: true
                    // }
                ]
            });
            this.autoScrolling = true;
        }

        onConfigsChanged() {

        }

        onLogin() {
            $("head").append(`
            <style id="styles-chat">
                :root {
                    ${"".concat(Object.entries(CSS_VARIABLES_DEFAULTS).map(([prop, rgb]) => `${prop}: ${rgb};\n`))}
                }
                #content {
                    & button {
                        box-shadow: none;
                    }
                    &.side-chat {
                        grid-template-columns: 2fr 10px minmax(300px, 4fr) !important;
                        > * #chat-increase-size-button, #chat-decrease-size-button {
                            display: none;
                        }
                        > #game-chat {
                            position: sticky; 
                            top: calc(12pt + 34pt); 
                            right: 0;
                            height: calc(100vh - 24pt - 34pt); 
                            overflow: auto;
                            grid-column: 3;
                            margin: 12pt 12pt 12pt 0pt;
                            background-color: rgb(var(--chat-outer-bg-color));
                        }
                    }
                }
                .resizer {
                    width: 16pt;
                    height: 100%;
                    background: transparent;
                    cursor: e-resize;
                    grid-column: 2;
                    margin: 0;
                }
                #chat-area {
                    background-color: rgb(var(--chat-bg-color));
                    border-color: rgb(var(--chat-bg-color));
                    resize: none;
                    > div {
                        font-size: 13pt;
                        font-family: 'montserrat', sans-serif;
                        font-weight: 500;
                        color: rgba(var(--chat-text-color), 1.0);
                        word-break: break-word;
                        > img {
                            vertical-align: text-bottom;
                        }
                        > a.chat-username {
                            font-weight: bolder;
                            color: rgba(var(--chat-username-color), 1.0);

                            &.highlighted {
                                color: #9444c9 !important;
                            }
                        }
                        &:has(span.server_message) {
                            color: rgba(var(--chat-server-msg-color), 1.0);
                            > span.server_message {
                                color: rgba(var(--chat-server-msg-tag-color), 1.0);
                                background-color: rgba(var(--chat-server-msg-tag-bg-color), 1.0);
                            }
                        }
                        > .color-green {
                            color: rgba(var(--chat-timestamp-color), 1.0);
                        }
                        > .color-grey {
                            color: rgba(var(--chat-level-color), 1.0);
                        }
                        &.dimmed {
                            color: rgba(var(--chat-text-color), 0.2) !important;
                            > span, a {
                                color: rgba(var(--chat-text-color), 0.3) !important;
                                border-color: rgba(var(--chat-text-color), 0.3);
                                background-color: transparent;
                                box-shadow: none;
                            }
                            > .chat-username {
                                color: rgba(var(--chat-username-color), 0.2) !important;
                            }
                            > img {
                                opacity: 0.2;
                            }
                        }
                        > span:not(:first-child):not(:empty) {
                            border-radius: 5pt;
                            display: inline-block;
                            height: 1.5em;
                            padding: 0px 4px 0px 4px;
                        }
                    }
                    &::-webkit-scrollbar {
                        background: transparent;
                        width: 0.25em;
                    }   
                    &::-webkit-scrollbar-track {
                        background: transparent;
                    }
                    &::-webkit-scrollbar-thumb {
                        background: rgba(var(--chat-text-color), 0.2);
                        border-radius: 5pt;
                    }
                }
                div:has(> .chat-area-input) {
                    display: flex;
                    flex-wrap: nowrap;
                    > .chat-area-input {
                        flex-grow: 1;
                    }
                }
            </style>
            `);

            const autoScrollButton = document.getElementById("chat-auto-scroll-button");

            const gameChat = document.getElementById("game-chat");
            const gameScreen = document.getElementById("game-screen");
            const topBar = document.getElementById("top-bar");

            autoScrollButton.style.removeProperty("max-height");
            autoScrollButton.style.color = "green";
            autoScrollButton.style.marginRight = "0.3em";
            
            // Autoscrolling detection
            const chatArea = document.getElementById("chat-area");
            chatArea.addEventListener("scroll", () => {
                const sh = chatArea.scrollHeight;
                const st = chatArea.scrollTop;
                const ht = chatArea.offsetHeight - 2; // Account for border width
                if(st == sh - ht) {
                    if(!this.autoScrolling) {
                        this.autoScrolling = true;
                        Chat.toggle_auto_scroll();
                    }
                }
                else {
                    if(this.autoScrolling) {
                        this.autoScrolling = false;
                        Chat.toggle_auto_scroll();
                    }
                }
            });

            document.querySelector("button[onclick='Chat.send()']").textContent = "\u27a4";
            document.querySelector(`button[onclick="switch_panels('panel-tags')"]`).innerHTML = `<img style="height: 24px;" src="https://d1xsc8x7nc5q8t.cloudfront.net/images/diamond.png"></img>`;
            document.querySelector(`button[onclick="switch_panels('panel-sigils')"]`).innerHTML = `<img style="height: 24px;" src="https://d1xsc8x7nc5q8t.cloudfront.net/images/forest_ent_sigil_chat.png">`;

            // Observer to modify chat messages as they come in
            new window.MutationObserver((mutationRecords) => {
                const usernameElements = chatArea.getElementsByClassName("chat-username");
                mutationRecords.forEach(record => {
                    if(record.addedNodes.length > 0 && record.addedNodes[0].nodeType == 1){
                        const newUserMessage = record.addedNodes[0].querySelector(".chat-username");
                        const newServerMessage = record.addedNodes[0].querySelector(".server_message");
                        if(newUserMessage) {
                            newUserMessage.addEventListener("contextmenu", function(event) {
                                const clickedElement = event.target;
                                const isHighlighted = clickedElement.classList.contains("highlighted");
                                Array.from(chatArea.getElementsByClassName("chat-username"))
                                    .map(e => { if(e.classList.contains("highlighted")) e.classList.remove("highlighted"); return e; })
                                    .map(e => { if(e.parentNode.classList.contains("dimmed")) e.parentNode.classList.remove("dimmed"); return e; })
                                    .map(e => { if(!isHighlighted) e.textContent === clickedElement.textContent ? e.classList.add("highlighted"): e.parentNode.classList.add("dimmed"); return e; });
                                event.preventDefault();
                                return false;
                            });
                            const currentlyHighlightedElements = Array.from(usernameElements).filter(e => e.classList.contains("highlighted"));
                            if(currentlyHighlightedElements.length > 0) {
                                // A username is currently highlighted, so highlight the new message if its the same name, or dim it otherwise
                                (newUserMessage.textContent === currentlyHighlightedElements[0].textContent) ? newUserMessage.classList.add("highlighted") : newUserMessage.parentNode.classList.add("dimmed");
                            }
                        }
                        if(newServerMessage) {
                            newServerMessage.parentNode.style.removeProperty("color");
                        }
                    }
                });
            }).observe(chatArea, {
                childList: true,
                subtree: true
            });

            // Observer to detect side-chat mode change
            new window.MutationObserver((mutationRecords) => {
                mutationRecords.forEach(record => {
                    if(record.type === "attributes" && record.attributeName === "class") {
                        //console.log(`Chat mode changed: ${record.target.classList.contains("side-chat") ? "Side" : "Bottom"}`);
                        this.configureChat(record.target.classList.contains("side-chat") ? "side" : "bottom");
                    }
                });
            }).observe(document.getElementById("content"), {
                attributes: true
            });

            // Observer to size game chat according to top bar height
            new window.MutationObserver((mutationRecords) => {
                mutationRecords.forEach(record => {
                    if(record.attributeName === "style") {
                        gameChat.style.top = `calc(12pt + ${topBar.clientHeight + 1}px)`;
                        gameChat.style.height = `calc(100vh - 24pt - ${topBar.clientHeight + 1}px)`;
                    }
                });
            }).observe(topBar, {
                attributes: true
            });

            // Take all modals out of the game-chat div to prevent sticky chat area from breaking them
            gameChat.querySelectorAll(".modal").forEach(modal => {
                const e = gameChat.removeChild(modal);
                gameChat.after(e);
            });

            // Move top bar outside content so it goes over the chat
            document.getElementById("content").before(gameScreen.removeChild(document.getElementById("top-bar")));

            if(IdlePixelPlus.plugins["ui-tweaks"]) {
                this.applyUITweaksThemes();
                this.overrideUITweaksFunctions();
            }
         }

        configureChat(setting) {
            const gameScreen = document.getElementById("game-screen");
            const gameChat = document.getElementById("game-chat");
            const resizerDiv = document.getElementById("chat-resizer");

            // Add resize drag bar to the left of the chat
            if(setting === "side") {
                gameChat.classList.remove("m-3");
                if(!resizerDiv) {
                    $("#game-chat").before(`<div id="chat-resizer" class="resizer"></div>`);
                    document.getElementById("chat-resizer").addEventListener("mousedown", initDrag, false);
                }

                gameScreen.style.width = "70vw";
            }
            else {
                if(resizerDiv)
                    resizerDiv.remove();
                if(!gameChat.classList.contains("m-3"))
                    gameChat.classList.add("m-3");

                gameScreen.style.width = "100vw";
            }

            function initDrag(e) { 
                document.documentElement.addEventListener("mousemove", doDrag, false);
                document.documentElement.addEventListener("mouseup", stopDrag, false);
                document.body.style.cursor = 'e-resize';
            }

            function doDrag(e) {
                const panelsElement = document.getElementById("panels");
                const menuBarElement = document.getElementById("menu-bar");
                const minGameScreenWidth = panelsElement.offsetWidth + menuBarElement.offsetWidth + 4;
                e.preventDefault();
                if(gameScreen.offsetWidth < minGameScreenWidth && e.clientX < minGameScreenWidth)
                    gameScreen.style.width = (minGameScreenWidth - 1) + "px";
                else if(e.clientX > window.innerWidth - 324)
                    return;
                else
                    gameScreen.style.width = e.clientX + "px";
            }

            function stopDrag() {
                document.documentElement.removeEventListener("mousemove", doDrag, false);
                document.documentElement.removeEventListener("mouseup", stopDrag, false);
                document.body.style.cursor = '';
            }
        }

        onPanelChanged(_, panelAfter) {
            // Resize chat automatically if overlapping with the panel
            const gameScreen = document.getElementById("game-screen");
            const rightLimit = document.getElementById(`panel-${panelAfter}`).getBoundingClientRect().right;
            if(!/vw/.test(gameScreen.style.width) && rightLimit > parseInt(gameScreen.style.width.replace(/[^0-9]/g, ""))) {
                gameScreen.style.width = `${rightLimit + 24}px`;
            }
        }

        hexToRgb(hex) {
            const bigint = parseInt(hex.replace(/#/, ""), 16);
            const r = (bigint >> 16) & 255;
            const g = (bigint >> 8) & 255;
            const b = bigint & 255;
        
            return r + ", " + g + ", " + b;
        }

        applyUITweaksThemes() {
            const uitConfig = IdlePixelPlus.plugins["ui-tweaks"].config;
            const root = document.documentElement;
            //console.log(uitConfig);

            const uitCustomizationConfigMap = [
                {enable: "font-color-enabled-chat-area-chat-username",  config: "font-color-chat-area-chat-username",   cssProperty: "--chat-username-color"},
                {enable: "font-color-enabled-chat-area",                config: "font-color-chat-area",                 cssProperty: "--chat-text-color"},
                {enable: "font-color-enabled-chat-area-server_message", config: "font-color-chat-area-server_message",  cssProperty: "--chat-server-msg-tag-color"},
                {enable: "color-enabled-chat-area-server_message",      config: "color-chat-area-server_message",       cssProperty: "--chat-server-msg-tag-bg-color"},
                {enable: "serverMessageTextOverrideEnabled",            config: "serverMessageTextOverrideColor",       cssProperty: "--chat-server-msg-color"},
                {enable: "font-color-enabled-chat-area-color-green",    config: "font-color-chat-area-color-green",     cssProperty: "--chat-timestamp-color"},
                {enable: "font-color-enabled-chat-area-color-grey",     config: "font-color-chat-area-color-grey",      cssProperty: "--chat-level-color"},
                {enable: "color-enabled-chat-area",                     config: "color-chat-area",                      cssProperty: "--chat-bg-color"},
                {enable: "color-enabled-game-chat",                     config: "color-game-chat",                      cssProperty: "--chat-outer-bg-color"}
            ];

            uitCustomizationConfigMap.forEach((customization) => {
                if(uitConfig[customization.enable])
                    root.style.setProperty(customization.cssProperty, this.hexToRgb(uitConfig[customization.config]));
                else
                    root.style.setProperty(customization.cssProperty, CSS_VARIABLES_DEFAULTS[customization.cssProperty]);
            });
        }

        overrideUITweaksFunctions() {
            // Wrap UIT's onConfigChanged to set css properties accordingly
            const originalOnConfigsChanged = IdlePixelPlus.plugins["ui-tweaks"].onConfigsChanged;
            IdlePixelPlus.plugins["ui-tweaks"].onConfigsChanged = function() {
                originalOnConfigsChanged.apply(this, arguments);
                IdlePixelPlus.plugins["chat_overhaul"].applyUITweaksThemes();
            }
        }

        // This function is called when the player enters combat
        onCombatStart() {
            document.getElementById("top-bar").style.display = "none";
        }

        // This function is called when the player exits combat
        onCombatEnd() {
            document.getElementById("top-bar").style.display = "block";
        }
    }

    const plugin = new ChatPlugin();
    IdlePixelPlus.registerPlugin(plugin);

})();