Greasy Fork 还支持 简体中文。

TORN: Better Chat

Improvements to the usability of chats 2.0.

À partir de 2023-10-30. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         TORN: Better Chat
// @namespace    dekleinekobini.betterchat
// @license      GPL-3
// @version      1.1.1
// @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-wrapper {
            display: flex;
            justify-content: space-between;
        }
        
        .better-chat-settings-title {
            display: block;
            font-size: 1.25em;
            font-weight: bold;
            margin-bottom: 2px;
        }
        
        .better-chat-settings-close-popup {
            padding-inline: 5px;
        }
        
        .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.textContent = title;

        const closeElement = document.createElement("button");
        titleElement.classList.add("better-chat-settings-close-popup");
        closeElement.textContent = "X";
        closeElement.addEventListener("click", () => {
            [...document.getElementsByClassName("better-chat-settings-overlay")].forEach(overlay => overlay.remove());
        });

        const titleWrapper =  document.createElement("div");
        titleWrapper.classList.add("better-chat-settings-title-wrapper");
        titleWrapper.appendChild(titleElement);
        titleWrapper.appendChild(closeElement);

        popup.appendChild(titleWrapper);
    }

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