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