TORN: Better Chat

Improvements to the usability of chats 2.0.

目前为 2023-10-28 提交的版本。查看 最新版本

// ==UserScript==
// @name         TORN: Better Chat
// @namespace    dekleinekobini.betterchat
// @license      GPL-3
// @version      1.1.0
// @description  Improvements to the usability of chats 2.0.
// @author       DeKleineKobini [2114440]
// @match        https://www.torn.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @run-at       document-start
// @grant        GM_addStyle
// ==/UserScript==

"use strict";

const SETTINGS_KEY = "better-chat-settings";
// Settings have been moved to the in-game settings window.
const DEFAULT_SETTINGS = {
    messages: {
        hideAvatars: true,
        compact: true,
        leftAlignedText: true, // left align all text, prefixed by the name (supports the mini-profile as well), even for private chats
        highlight: [
            // Colors can be specified as:
            // - hex color (include #, only full format = 6 numbers)
            // - custom colors (check below); "torntools-green"
            // Search is just text, except "%player%" where it used the current players name.
            {id: 0, color: "torntools-green", search: "%player%"},
        ],
    },
    box: {
        groupRight: true, // opening chat logic to put private chat left of group chats
        hideAvatars: true,
    },
};
let localSettings = loadSettings();

const TEXT_COLORS = {
    "torntools-green": "#7ca900",
};

(() => {
    setupStyles();
    setupChatModifier().catch((reason) => console.error("[Better Chat] Failed to initialize the chat modifier.", reason));
    setupScriptSettings().catch((reason) => console.error("[Better Chat] Failed to initialize the settings tab.", reason));
})();

function includeStyle(styleRules) {
    if (typeof GM_addStyle !== "undefined") {
        GM_addStyle(styleRules);
    } else {
        const styleElement = document.createElement("style");
        styleElement.setAttribute("type", "text/css");
        styleElement.innerHTML = styleRules;
        document.head.appendChild(styleElement);
    }
}

function setupStyles() {
    if (localSettings.messages.hideAvatars) {
        includeStyle(`
            [class*='chat-box-body__avatar___'] {
                display: none;
            }
        `);
    }
    if (localSettings.messages.compact) {
        includeStyle(`
            [class*='chat-box-body__wrapper___'] {
                margin-bottom: 0px !important;
            }

            [class*='chat-box-body___'] > div:last-child {
                margin-bottom: 8px !important;
            }
        `);
    }
    if (localSettings.box.groupRight) {
        includeStyle(`
            [class*='group-chat-box___'] {
                gap: 3px;
            }

            [class*='group-chat-box__chat-box-wrapper___'] {
                margin-right: 0 !important;
            }
        `);
    }

    if (localSettings.messages.leftAlignedText) {
        includeStyle(`
            [class*='chat-box-body__sender___'] {
                display: unset !important;
                font-weight: 700;
            }

            [class*='chat-box-body__message-box___'] [class*='chat-box-body__sender___'] {
                margin-right: 4px;
            }

            [class*='chat-box-body__message-box___'] {
                background: none !important;
                border-radius: none !important;
                color: initial !important;
                padding: 0 !important;
            }

            [class*='chat-box-body__message-box--self___'] {
                background: none !important;
                border-radius: none !important;
                color: initial !important;
                padding: 0 !important;
            }

            [class*='chat-box-body__wrapper--self___'] {
                justify-content: normal !important;
            }

            [class*='chat-box-body__wrapper--self___'] > [class*='chat-box-body__message___'],
            [class*='chat-box-body__message___'] {
                color: var(--chat-text-color) !important;
            }
        `);
    }

    if (localSettings.box.hideAvatars) {
        includeStyle(`
            [class*='avatar__avatar-status-wrapper___'] > img {
                display: none;
            }
        `);
    }

    includeStyle(`
        #better-chat-settings-icon button {
            color: #f7f7f7;
        }
        
        .better-chat-settings-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .better-chat-settings-popup {
            background-color: #f7f7f7;
            width: 300px;
            height: 300px;
            padding: 4px;
        }
        
        .better-chat-settings-title {
            display: block;
            font-size: 1.25em;
            font-weight: bold;
            margin-bottom: 2px;
        }
        
        .better-chat-settings-description {
            display: block;
            font-size: 0.9em;
            margin-bottom: 2px;
        }
        
        .better-chat-settings-subtitle {
            display: block;
            font-weight: bold;
            margin-bottom: 2px;
        }
        
        .better-chat-settings-subtitle:not(:first-child) {
            margin-top: 4px;
        }
        
        .better-chat-settings-popup > section {
            display: flex;
            align-items: center;
            gap: 2px;
            margin-bottom: 1px;
        }
        
        .better-chat-settings-popup button {
            cursor: pointer;
        }
        
        .better-chat-settings-highlight-entry {
            display: flex;
            gap: 4px;
        }
    `);
}

async function setupChatModifier() {
    const group = await new Promise((resolve) => {
        new MutationObserver((_, observer) => {
            const group = findByClass(document, "group-chat-box___");
            if (group) {
                observer.disconnect();
                resolve(group);
            }
        }).observe(document, {childList: true, subtree: true});
    });

    group.childNodes.forEach(processChat)
    new MutationObserver((mutations) => {
        mutations.flatMap((mutation) => [...mutation.addedNodes]).forEach(processChat);
    }).observe(group, {childList: true});
}

function processChat(chatNode) {
    if (localSettings.box.groupRight) {
        const avatarElement = findByClass(chatNode, "chat-box-header__avatar___", "> *");
        const isGroup = avatarElement.tagName.toLowerCase() === "svg";

        if (isGroup) {
            chatNode.style.order = "1";
        }
    }

    const bodyElement = findByClass(chatNode, "chat-box-body___");
    bodyElement.childNodes.forEach(processMessage);
    new MutationObserver((mutations) => {
        mutations.flatMap((mutation) => [...mutation.addedNodes]).forEach(processMessage);
    }).observe(chatNode, {childList: true});
    new MutationObserver(() => {
        bodyElement.childNodes.forEach(processMessage);
    }).observe(bodyElement, {childList: true});
}

function getCurrentPlayername() {
    const websocketElement = document.getElementById("websocketConnectionData");
    if (websocketElement) {
        const data = JSON.parse(websocketElement.textContent);

        return data.playername;
    }

    const sidebarElement = findByClass(document, "menu-value___");
    if (sidebarElement) {
        // Get name from the sidebar
        return sidebarElement.textContent;
    }

    const attackerElement = document.querySelector(".user-name.left");
    if (attackerElement) {
        // Attack name
        return attackerElement.textContent;
    }

    return "Yourself";
}

function processMessage(messageNode) {
    if (messageNode.querySelector(".color-chatError")) {
        // This is a "Connecting to the server" message, don't process it.
        return;
    }

    const senderElement = findByClass(messageNode, "chat-box-body__sender___");
    const messageElement = findByClass(messageNode, "chat-box-body__message___");

    const currentPlayer = getCurrentPlayername();
    let senderName = senderElement.textContent.substring(0, senderElement.textContent.length - 1);
    if (senderName === "newMessage") {
        // Take the name from the sidebar.
        senderElement.textContent = `${currentPlayer}:`;
        senderName = currentPlayer;
    }

    if (localSettings.messages.highlight.length > 0) {
        const highlights = localSettings.messages.highlight
            .map(({search, color}) => ({
                search: search.replaceAll("%player%", currentPlayer),
                color: convertColor(color),
            }));

        const nameHighlight = highlights.find(({search}) => senderName.toLowerCase() === search.toLowerCase());
        if (nameHighlight) {
            senderElement.setAttribute("style", `background-color: ${nameHighlight.color} !important;`);
        }

        const messageHighlight = highlights.find(({search}) => messageElement.textContent.toLowerCase().includes(search.toLowerCase()));
        if (messageHighlight) {
            const wrapperElement = findByClass(messageNode, "chat-box-body__wrapper___");

            wrapperElement.setAttribute("style", `background-color: ${messageHighlight.color} !important;`);
        }
    }
}

function baseColor(color) {
    if (color in TEXT_COLORS) color = TEXT_COLORS[color];

    return color;
}

function convertColor(color) {
    color = baseColor(color);

    return color.length === 7 ? `${color}6e` : color;
}

function findByClass(node, className, subSelector = "") {
    return node.querySelector(`[class*='${className}'] ${subSelector}`.trim())
}

async function setupScriptSettings() {
    const chatList = await new Promise((resolve) => {
        new MutationObserver((_, observer) => {
            const group = findByClass(document, "chat-app__chat-list-chat-box-wrapper___");
            if (group) {
                observer.disconnect();
                resolve(group);
            }
        }).observe(document, {childList: true, subtree: true});
    });

    handleAppPanel(chatList);
    new MutationObserver(() => {
        handleAppPanel(chatList);
    }).observe(chatList, {childList: true});
}

function handleAppPanel(chatlist) {
    const panel = findByClass(chatlist, "chat-app__panel___");
    if (!panel) return;

    modifyAppPanel(panel);
    new MutationObserver(() => {
        modifyAppPanel(panel);
    }).observe(panel, {childList: true});
}

function modifyAppPanel(panel) {
    if (panel.classList.contains("better-chat-settings")) return;

    panel.classList.add("better-chat-settings")

    const icon = createScriptSettingsIcon();

    const panelHeader = findByClass(panel, "chat-list-header__actions___");
    panelHeader.insertAdjacentElement("afterbegin", icon)
}

function createScriptSettingsIcon() {
    const icon = document.createElement("div");
    icon.id = "better-chat-settings-icon";
    icon.classList.add("chat-list-header__action-wrapper___b5asl");
    icon.innerHTML = `
        <button type="button" class="chat-list-header__button___vfszc">
            BS
        </button>
    `;
    icon.addEventListener("click", (event) => {
        event.stopPropagation();
        showScriptSettings();
    }, {capture: true});

    return icon;
}

function showScriptSettings() {
    if (document.querySelector(".better-chat-settings-overlay")) return;

    const popup = createPopup();

    const overlay = createOverlay();
    overlay.appendChild(popup);
    document.body.appendChild(overlay);
}

function loadSettings() {
    const storedSettings = localStorage.getItem(SETTINGS_KEY);
    const settings = storedSettings ? JSON.parse(storedSettings) : {};

    const defaultSettings = DEFAULT_SETTINGS;
    mergeRecursive(defaultSettings, settings);

    return defaultSettings;
}

function mergeRecursive(target, otherObject) {
    for (const key in otherObject) {
        try {
            if (typeof otherObject[key] == "object" && !Array.isArray(otherObject[key])) {
                target[key] = mergeRecursive(target[key], otherObject[key]);
            } else {
                target[key] = otherObject[key];
            }
        } catch (e) {
            target[key] = otherObject[key];
        }
    }

    return target;
}

function createPopup() {
    const popup = document.createElement("div");
    popup.classList.add("better-chat-settings-popup");

    appendTitle("Better Chat - Settings");
    appendDescription("You can change your Better Chat settings here. Reload after changes to apply them.");

    appendSubtitle("Messages");
    appendCheckbox(
        "messages-hideAvatars",
        "Hide avatars in the messages.",
        () => localSettings.messages.hideAvatars,
        (newValue) => {
            localSettings.messages.hideAvatars = newValue;
        },
    );
    appendCheckbox(
        "messages-compact",
        "Make the chat significantly compacter.",
        () => localSettings.messages.compact,
        (newValue) => {
            localSettings.messages.compact = newValue;
        },
    );
    appendCheckbox(
        "messages-leftAlignedText",
        "Left align all messages.",
        () => localSettings.messages.leftAlignedText,
        (newValue) => {
            localSettings.messages.leftAlignedText = newValue;
        },
    );

    appendSubtitle("Boxes");
    appendCheckbox(
        "box-groupRight",
        "Move group chats to always be to the right of private chats.",
        () => localSettings.box.groupRight,
        (newValue) => {
            localSettings.box.groupRight = newValue;
        },
    );
    appendCheckbox(
        "box-hideAvatars",
        "Move avatars in the boxes.",
        () => localSettings.box.hideAvatars,
        (newValue) => {
            localSettings.box.hideAvatars = newValue;
        },
    );

    appendSubtitle("Highlights");
    appendHighlightList(
        () => localSettings.messages.highlight,
        ({search, color}) => {
            localSettings.messages.highlight = localSettings.messages.highlight.filter((highlight) => highlight.search !== search && highlight.color !== color)
        },
        (item) => localSettings.messages.highlight.push(item),
    );

    return popup;

    function appendTitle(title) {
        const titleElement = document.createElement("span");
        titleElement.classList.add("better-chat-settings-title");
        titleElement.innerText = title;

        popup.appendChild(titleElement);
    }

    function appendDescription(title) {
        const titleElement = document.createElement("span");
        titleElement.classList.add("better-chat-settings-description");
        titleElement.innerText = title;

        popup.appendChild(titleElement);
    }

    function appendSubtitle(title) {
        const titleElement = document.createElement("span");
        titleElement.classList.add("better-chat-settings-subtitle");
        titleElement.innerText = title;

        popup.appendChild(titleElement);
    }

    function appendCheckbox(id, labelText, valueGetter, valueSetter) {
        const inputId = `setting-${id}`;

        const input = document.createElement("input");
        input.checked = valueGetter();
        input.id = inputId;
        input.type = "checkbox";
        input.addEventListener("change", (event) => {
            valueSetter(event.currentTarget.checked);
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(localSettings));
        }, {capture: true});

        const label = document.createElement("label");
        label.setAttribute("for", inputId);
        label.innerText = labelText;

        const section = document.createElement("section");
        section.appendChild(input);
        section.appendChild(label);

        popup.appendChild(section);
    }

    function appendHighlightList(valueGetter, valueRemover, valueAdder) {
        const list = document.createElement("ul");

        valueGetter().forEach((item) => appendRow(item));

        const addButton = document.createElement("button");
        addButton.textContent = "add";
        addButton.addEventListener("click", () => {
            const item = {search: "%player%", color: "#7ca900"};

            valueAdder(item);
            appendRow(item, true);
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(localSettings));
        });
        list.appendChild(addButton);

        popup.appendChild(list);

        function appendRow(item, beforeButton = false) {
            const itemElement = document.createElement("li");
            itemElement.classList.add("better-chat-settings-highlight-entry");

            const searchInput = document.createElement("input");
            searchInput.type = "text";
            searchInput.placeholder = "Search...";
            searchInput.value = item.search;
            searchInput.addEventListener("change", (event) => {
                item.search = event.currentTarget.value;
                localStorage.setItem(SETTINGS_KEY, JSON.stringify(localSettings));
            });
            itemElement.appendChild(searchInput);

            const colorInput = document.createElement("input");
            colorInput.type = "color";
            colorInput.value = baseColor(item.color);
            colorInput.addEventListener("change", (event) => {
                item.color = event.currentTarget.value;
                localStorage.setItem(SETTINGS_KEY, JSON.stringify(localSettings));
            });
            itemElement.appendChild(colorInput);

            const removeButton = document.createElement("button");
            removeButton.textContent = "remove";
            removeButton.addEventListener("click", () => {
                itemElement.remove();
                valueRemover(item);
                localStorage.setItem(SETTINGS_KEY, JSON.stringify(localSettings));
            });
            itemElement.appendChild(removeButton);

            if (beforeButton) {
                list.insertBefore(itemElement, addButton);
            } else {
                list.appendChild(itemElement);
            }
        }
    }
}

function createOverlay() {
    const overlay = document.createElement("div");
    overlay.classList.add("better-chat-settings-overlay");
    overlay.addEventListener("click", (event) => {
        if (event.target !== overlay) return;

        overlay.remove();
    }, {once: true});

    return overlay;
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址