// ==UserScript==
// @name TORN: Better Chat
// @namespace dekleinekobini.betterchat
// @license GPL-3
// @version 1.0.5
// @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 = {
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.
{color: "torntools-green", search: "%player%"},
]
},
box: {
groupRight: true, // opening chat logic to put private chat left of group chats
hideAvatars: true,
},
};
const TEXT_COLORS = {
"torntools-green": "#7ca900",
};
const URL_PATTERN = /(https?:\/\/)?(\.?[\w-]+)+\.([a-z]{2,})(\/[\w\.]*)*(\?[\w=]+)?(#[\w/=]+)?/gi;
(() => {
setupStyles();
setupChatModifier().catch((reason) => console.error("[Better Chat] Failed to initialize the chat modifier.", 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 (settings.messages.hideAvatars) {
includeStyle(`
[class*='chat-box-body__avatar___'] {
display: none;
}
`);
}
if (settings.messages.compact) {
includeStyle(`
[class*='chat-box-body__wrapper___'] {
margin-bottom: 0px !important;
}
[class*='chat-box-body___'] > div:last-child {
margin-bottom: 8px !important;
}
`);
}
if (settings.box.groupRight) {
includeStyle(`
[class*='group-chat-box___'] {
gap: 3px;
}
[class*='group-chat-box__chat-box-wrapper___'] {
margin-right: 0 !important;
}
`);
}
if (settings.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 (settings.box.hideAvatars) {
includeStyle(`
[class*='avatar__avatar-status-wrapper___'] > img {
display: none;
}
`);
}
}
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 (settings.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 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 = findByClass(document, "menu-value___")?.textContent ?? "Yourself";
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 (settings.messages.highlight.length > 0) {
const highlights = settings.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 convertColor(color) {
if (color in TEXT_COLORS) color = TEXT_COLORS[color];
return color.length === 7 ? `${color}6e` : color;
}
function findByClass(node, className, subSelector = "") {
return node.querySelector(`[class*='${className}'] ${subSelector}`.trim())
}