TORN: Better Chat

Improvements to the usability of chats 2.0.

目前為 2023-10-28 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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;
}