TORN: Better Chat

Improvements to the usability of chats 2.0.

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

(function () {
  'use strict';

  var __defProp = Object.defineProperty;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => {
    __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
    return value;
  };
  const CHAT_SELECTORS = Object.freeze({
    /*
     * Global
     */
    CHAT_ROOT_ID: "chatRoot",
    AVATAR_WRAPPER_CLASS: "avatar__avatar-status-wrapper___",
    /*
     * Chat Boxes
     */
    GROUP_MENU_DESKTOP_CLASS: "minimized-menus__desktop___",
    GROUP_MENU_MOBILE_CLASS: "minimized-menus__mobile___",
    GROUP_MENU_MOBILE_BUTTON_CLASS: "minimized-menus__mobile-button___",
    CHAT_LIST_CLASS: "chat-app__chat-list-chat-box-wrapper___",
    CHAT_LIST_GROUP_CHAT_CLASS: "minimized-menu-item___",
    CHAT_LIST_NOTEPAD_CLASS: "chat-note-button___",
    CHAT_LIST_SETTINGS_CLASS: "chat-setting-button___",
    CHAT_LIST_PEOPLE_CLASS: "chat-list-button___",
    CHAT_MESSAGE_COUNT_CLASS: "message-count___",
    CHAT_WRAPPER_WRAPPER_CLASS: "group-chat-box___",
    CHAT_WRAPPER_CLASS: "group-chat-box__chat-box-wrapper___",
    CHAT_HEADER_CLASS: "chat-box-header___",
    CHAT_HEADER_INFO_BUTTON_CLASS: "chat-box-header__info-btn___",
    CHAT_HEADER_INFO_CLASS: "chat-box-header__info___",
    CHAT_HEADER_AVATAR_CLASS: "chat-box-header__avatar___",
    CHAT_HEADER_MINIMIZE_ICON_CLASS: "minimize-icon",
    CHAT_HEADER_CLOSE_ICON_CLASS: "close-icon",
    CHAT_BODY_CLASS: "chat-box-body___",
    /*
     * Messages
     */
    COLOR_ERROR_CLASS: "color-chatError",
    MESSAGE_BODY_WRAPPER_CLASS: "chat-box-message___",
    MESSAGE_BODY_WRAPPER_SELF_CLASS: "chat-box-message--self___",
    MESSAGE_BODY_CLASS: "chat-box-message__box___",
    MESSAGE_BODY_SELF_CLASS: "chat-box-message__box--self___",
    MESSAGE_SENDER_CLASS: "chat-box-message__sender___",
    MESSAGE_MINIMIZED_BOX_AVATAR_CLASS: "minimized-chat-box__avatar___",
    MESSAGE_AVATAR_CLASS: "chat-box-message__avatar___",
    MESSAGE_CONTENT_WRAPPER_CLASS: "chat-box-message__message___",
    MESSAGE_SEND_BUTTON_CLASS: "chat-box-footer__send-icon-wrapper___",
    LAST_MESSAGE_TIMESTAMP_CLASS: "chat-box-body__lastmessage-timestamp___",
    /*
     * People Panel
     */
    PANEL_WRAPPER_CLASS: "chat-app__panel___",
    /*
     * People Panel
     */
    PEOPLE_PANEL_LOADING: "chat-tab__loader___",
    PEOPLE_PANEL_CLASS: "chat-tab___",
    PEOPLE_PANEL_CLOSE_BUTTON_ID: "_close_default_dark",
    PEOPLE_PANEL_SETTINGS_BUTTON_ID: "setting_default",
    PEOPLE_PANEL_HEADER_BUTTON: "chat-list-header__button___",
    PEOPLE_PANEL_TABS_WRAPPER_CLASS: "chat-list-header__tabs___",
    PEOPLE_PANEL_TAB_ACTIVE_CLASS: "chat-list-header__tab--active___",
    PEOPLE_PANEL_TAB_CONTENT_CLASS: "chat-tab-content___",
    PEOPLE_PANEL_MEMBER_CARD_CLASS: "member-card___",
    PEOPLE_PANEL_STATUS_ONLINE_CLASS: "online-status--online___",
    PEOPLE_PANEL_STATUS_IDLE_CLASS: "online-status--idle___",
    PEOPLE_PANEL_STATUS_OFFLINE_CLASS: "online-status--offline___",
    /*
     * Settings Panel
     */
    SETTINGS_PANEL_CLASS: "settings-panel___",
    SETTINGS_PANEL_HEADER_CLASS: "settings-header___",
    SETTINGS_PANEL_HEADER_TITLE_CLASS: "settings-header__text-container___",
    SETTINGS_PANEL_CLOSE_BUTTON_CLASS: "settings-header__close-button___"
  });
  function isElement(node) {
    return node.nodeType === Node.ELEMENT_NODE;
  }
  function isHTMLElement(node) {
    return isElement(node) && node instanceof HTMLElement;
  }
  function findByPartialClass(node, className, subSelector = "") {
    return node.querySelector(`[class*='${className}'] ${subSelector}`.trim());
  }
  function findAll(node, selector) {
    return [...node.querySelectorAll(selector)];
  }
  function findAllByPartialClass(node, className, subSelector = "") {
    return findAll(node, `[class*='${className}'] ${subSelector}`.trim());
  }
  async function findDelayed(node, findElement, timeout) {
    return new Promise((resolve, reject) => {
      const initialElement = findElement();
      if (initialElement) {
        resolve(initialElement);
        return;
      }
      const observer = new MutationObserver(() => {
        const element = findElement();
        element && (clearTimeout(timeoutId), observer.disconnect(), resolve(element));
      }), timeoutId = setTimeout(() => {
        observer.disconnect(), reject("Failed to find the element within the acceptable timeout.");
      }, timeout);
      observer.observe(node, { childList: true, subtree: true });
    });
  }
  async function findByPartialClassDelayed(node, className, subSelector = "", timeout = 5e3) {
    return findDelayed(node, () => findByPartialClass(node, className, subSelector), timeout);
  }
  async function findBySelectorDelayed(node, selector, timeout = 5e3) {
    return findDelayed(node, () => node.querySelector(selector), timeout);
  }
  function pluralize(word, amount) {
    return amount === 1 ? `${amount} ${word}` : `${amount} ${word}s`;
  }
  async function sleep(millis) {
    return new Promise((resolve) => setTimeout(resolve, millis));
  }
  function mergeRecursive(input, otherObject) {
    const target = JSON.parse(JSON.stringify(input));
    return Object.entries(otherObject).forEach(([key, value]) => {
      try {
        typeof value == "object" && !Array.isArray(value) ? target[key] = mergeRecursive(input[key], value) : target[key] = value;
      } catch {
        target[key] = value;
      }
    }), target;
  }
  let currentPlayerName;
  function getCurrentPlayerName() {
    if (currentPlayerName)
      return currentPlayerName;
    const websocketElement = document.getElementById("websocketConnectionData");
    if (websocketElement) {
      const data = JSON.parse(websocketElement.textContent);
      return currentPlayerName = data.playername, data.playername;
    }
    const sidebarElement = findByPartialClass(document, "menu-value___");
    if (sidebarElement)
      return currentPlayerName = sidebarElement.textContent, sidebarElement.textContent;
    const attackerElement = document.querySelector(".user-name.left");
    if (attackerElement)
      return currentPlayerName = attackerElement.textContent, attackerElement.textContent;
    const chatSenderElement = document.querySelector(
      `[class*='${CHAT_SELECTORS.MESSAGE_BODY_WRAPPER_SELF_CLASS}'] [class*='${CHAT_SELECTORS.MESSAGE_SENDER_CLASS}']`
    );
    if (chatSenderElement) {
      let name = chatSenderElement.textContent;
      return name = name.substring(0, name.length - 1), currentPlayerName = name, name;
    }
    return console.warn("[Playground] Failed to get the current player's name."), "unknown current player";
  }
  function notNull(value) {
    return value != null;
  }
  var _GM_addStyle = /* @__PURE__ */ (() => typeof GM_addStyle != "undefined" ? GM_addStyle : void 0)();
  const TEXT_COLORS = {
    "torntools-green": "#7ca900"
  };
  function baseColor(color) {
    if (color in TEXT_COLORS)
      return TEXT_COLORS[color];
    return color;
  }
  function convertColor(color) {
    const base = baseColor(color);
    return base.length === 7 ? `${base}6e` : base;
  }
  const SETTINGS_KEY = "better-chat-settings";
  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%" }
      ],
      fontSize: {
        enabled: false,
        size: 12
      }
    },
    box: {
      groupRight: true,
      // opening chat logic to put private chat left of group chats
      hideAvatars: true,
      nameAutocomplete: false,
      mobileGroups: false
    },
    people: {
      sortOnStatus: true
    },
    accessibility: {
      describeButtons: true,
      presentationSender: true
    }
  };
  function loadSettings() {
    const storedSettings = localStorage.getItem(SETTINGS_KEY);
    const settings2 = storedSettings ? JSON.parse(storedSettings) : {};
    return mergeRecursive(DEFAULT_SETTINGS, settings2);
  }
  function save() {
    localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
  }
  function showSettingsIcon(panel) {
    if (panel.querySelector("#better-chat-settings-icon"))
      return;
    const icon = createScriptSettingsIcon();
    const closeButton = findByPartialClass(panel, CHAT_SELECTORS.SETTINGS_PANEL_CLOSE_BUTTON_CLASS);
    if (!closeButton)
      return;
    closeButton.insertAdjacentElement("beforebegin", icon);
  }
  function createScriptSettingsIcon() {
    const button = document.createElement("button");
    button.type = "button";
    button.ariaLabel = "Better Chat settings";
    button.textContent = "BS";
    button.addEventListener(
      "click",
      (event) => {
        event.stopPropagation();
        showScriptSettings();
      },
      { capture: true }
    );
    const icon = document.createElement("div");
    icon.id = "better-chat-settings-icon";
    icon.appendChild(button);
    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);
    popup.focus();
  }
  function createPopup() {
    const popup = document.createElement("div");
    popup.classList.add("better-chat-settings-popup");
    popup.setAttribute("autofocus", "");
    popup.setAttribute("tabindex", "0");
    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.",
      () => settings.messages.hideAvatars,
      (newValue) => settings.messages.hideAvatars = newValue
    );
    appendCheckbox(
      "messages-compact",
      "Make the chat significantly compacter.",
      () => settings.messages.compact,
      (newValue) => settings.messages.compact = newValue
    );
    appendCheckbox(
      "messages-leftAlignedText",
      "Left align all messages.",
      () => settings.messages.leftAlignedText,
      (newValue) => settings.messages.leftAlignedText = newValue
    );
    {
      const inputId = `setting-font-size`;
      const checkbox = document.createElement("input");
      checkbox.checked = settings.messages.fontSize.enabled;
      checkbox.id = inputId;
      checkbox.type = "checkbox";
      checkbox.addEventListener(
        "change",
        () => {
          settings.messages.fontSize.enabled = checkbox.checked;
          save();
        },
        { capture: true }
      );
      const label = document.createElement("label");
      label.setAttribute("for", inputId);
      label.innerText = "Font size";
      const sizeInput = document.createElement("input");
      sizeInput.value = settings.messages.fontSize.size.toString();
      sizeInput.type = "number";
      sizeInput.addEventListener(
        "change",
        () => {
          settings.messages.fontSize.size = parseInt(sizeInput.value, 10);
          save();
        },
        { capture: true }
      );
      sizeInput.style.width = "40px";
      sizeInput.style.marginLeft = "2px";
      const section = document.createElement("section");
      section.appendChild(checkbox);
      section.appendChild(label);
      section.appendChild(sizeInput);
      popup.appendChild(section);
    }
    appendSubtitle("Boxes");
    appendCheckbox(
      "box-groupRight",
      "Move group chats to always be to the right of private chats.",
      () => settings.box.groupRight,
      (newValue) => settings.box.groupRight = newValue
    );
    appendCheckbox(
      "box-hideAvatars",
      "Hide avatars in the boxes.",
      () => settings.box.hideAvatars,
      (newValue) => settings.box.hideAvatars = newValue
    );
    appendCheckbox(
      "box-autocomplete",
      "Autocomplete when entering an message inside of a group box.",
      () => settings.box.nameAutocomplete,
      (newValue) => settings.box.nameAutocomplete = newValue
    );
    appendCheckbox(
      "box-mobileGroups",
      "Always show group chats on mobile.",
      () => settings.box.mobileGroups,
      (newValue) => settings.box.mobileGroups = newValue
    );
    appendSubtitle("Highlights");
    appendHighlightList(
      () => settings.messages.highlight,
      ({ search, color }) => {
        const removeIndex = settings.messages.highlight.findLastIndex((highlight) => highlight.search === search && highlight.color === color);
        settings.messages.highlight = settings.messages.highlight.filter((_, index) => index !== removeIndex);
      },
      (item) => settings.messages.highlight.push(item)
    );
    appendSubtitle("People");
    appendCheckbox(
      "people-sortOnStatus",
      "Sort players in the people tab based on status.",
      () => settings.people.sortOnStatus,
      (newValue) => settings.people.sortOnStatus = newValue
    );
    appendSubtitle("Accessibility");
    appendCheckbox(
      "accessibility-describeButtons",
      "Describe (most) buttons, for users with a screen reader.",
      () => settings.accessibility.describeButtons,
      (newValue) => settings.accessibility.describeButtons = newValue
    );
    appendCheckbox(
      "accessibility-presentationSender",
      "Don't read out the button role of the sender.",
      () => settings.accessibility.presentationSender,
      (newValue) => settings.accessibility.presentationSender = newValue
    );
    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");
      closeElement.classList.add("better-chat-settings-close-popup");
      closeElement.textContent = "X";
      closeElement.addEventListener("click", () => {
        [...document.getElementsByClassName("better-chat-settings-overlay")].forEach((overlay) => overlay.remove());
      });
      closeElement.ariaLabel = "Close better chat settings";
      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",
        () => {
          valueSetter(input.checked);
          save();
        },
        { 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);
        save();
      });
      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", () => {
          item.search = searchInput.value;
          save();
        });
        itemElement.appendChild(searchInput);
        const colorInput = document.createElement("input");
        colorInput.type = "color";
        colorInput.value = baseColor(item.color);
        colorInput.addEventListener("change", () => {
          item.color = colorInput.value;
          save();
        });
        itemElement.appendChild(colorInput);
        const removeButton = document.createElement("button");
        removeButton.textContent = "remove";
        removeButton.addEventListener("click", () => {
          itemElement.remove();
          valueRemover(item);
          save();
        });
        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;
  }
  const settings = loadSettings();
  class ScriptEventHandler {
    static onLoad() {
      new MutationObserver((_, observer) => {
        const group = findByPartialClass(document, CHAT_SELECTORS.CHAT_WRAPPER_WRAPPER_CLASS);
        if (!group)
          return;
        observer.disconnect();
        ScriptEventHandler.onChatLoad(group);
      }).observe(document, { childList: true, subtree: true });
      new MutationObserver((_, observer) => {
        const chatList = findByPartialClass(document, CHAT_SELECTORS.CHAT_LIST_CLASS);
        if (!chatList)
          return;
        ScriptEventHandler.handlePanel(chatList);
        new MutationObserver(() => ScriptEventHandler.handlePanel(chatList)).observe(chatList, { childList: true });
        observer.disconnect();
      }).observe(document, { childList: true, subtree: true });
      findBySelectorDelayed(document, "head").then(() => {
        StylingFeature.defaultStyles();
        StylingFeature.improveStyles();
        StylingFeature.mobileGroups();
        StylingFeature.hideAvatars();
        StylingFeature.compact();
        StylingFeature.groupRight();
        StylingFeature.leftAlignedText();
        StylingFeature.fontSize();
      }).catch((reason) => console.error("[BC] Failed to apply styles.", reason));
    }
    static onChatLoad(root) {
      root.childNodes.forEach((node) => ScriptEventHandler.onChatOpened(node));
      new MutationObserver(
        (mutations) => mutations.flatMap((mutation) => [...mutation.addedNodes]).filter(isHTMLElement).forEach(ScriptEventHandler.onChatOpened)
      ).observe(root, { childList: true });
      setInterval(ScriptEventHandler.onUnreadCountChange, 5e3);
      setTimeout(ScriptEventHandler.onUnreadCountChange, 1e3);
      ScriptEventHandler.onUnreadCountChange();
      AccessibilityFeature.describeRootPanels();
    }
    static onChatOpened(chat) {
      const bodyElement = findByPartialClass(chat, CHAT_SELECTORS.CHAT_BODY_CLASS);
      bodyElement.childNodes.forEach((node) => ScriptEventHandler.onMessageReceived(node));
      new MutationObserver((mutations) => {
        mutations.flatMap((mutation) => [...mutation.addedNodes]).filter(isHTMLElement).forEach(ScriptEventHandler.onMessageReceived);
      }).observe(chat, { childList: true });
      new MutationObserver(() => bodyElement.childNodes.forEach((node) => ScriptEventHandler.onMessageReceived(node))).observe(
        bodyElement,
        {
          childList: true
        }
      );
      AccessibilityFeature.describeChatButtons(chat);
      ChatGroupFeature.moveGroupRight(chat);
      ChatGroupFeature.nameAutocompletion(chat);
    }
    static onMessageReceived(message) {
      if (message.querySelector(`.${CHAT_SELECTORS.COLOR_ERROR_CLASS}`)) {
        return;
      }
      if (message instanceof HTMLElement && message.className.includes(CHAT_SELECTORS.LAST_MESSAGE_TIMESTAMP_CLASS)) {
        return;
      }
      const senderElement = findByPartialClass(message, CHAT_SELECTORS.MESSAGE_SENDER_CLASS);
      const currentPlayer = getCurrentPlayerName();
      let senderName = senderElement.textContent.substring(0, senderElement.textContent.length - 1);
      if (senderName === "newMessage") {
        senderElement.textContent = `${currentPlayer}:`;
        senderName = currentPlayer;
      }
      AccessibilityFeature.describeMessageButtons(message, senderName);
      MessageFeature.highlightMessages(message, senderName);
    }
    static onPeoplePanelLoad(panel) {
      AccessibilityFeature.appPanelAccessibility(panel);
    }
    static onSettingsPanelLoad(panel) {
      showSettingsIcon(panel);
    }
    static onPeopleListLoad(content) {
      PeopleStatusFeature.sortOnStatus(content);
    }
    static handlePanel(chatList) {
      var _a;
      const peoplePanel = (_a = chatList.querySelector(
        `[class*='${CHAT_SELECTORS.PANEL_WRAPPER_CLASS}'] [class*='${CHAT_SELECTORS.PEOPLE_PANEL_CLASS}']`
      )) == null ? void 0 : _a.parentElement;
      if (peoplePanel && !peoplePanel.querySelector(".better-chat-found")) {
        findByPartialClass(peoplePanel, CHAT_SELECTORS.PEOPLE_PANEL_CLASS).classList.add("better-chat-found");
        ScriptEventHandler.onPeoplePanelLoad(peoplePanel);
        new MutationObserver(() => {
          ScriptEventHandler.onPeoplePanelLoad(peoplePanel);
        }).observe(peoplePanel, { childList: true });
      }
      const settingsPanel = chatList.querySelector(`[class*='${CHAT_SELECTORS.PANEL_WRAPPER_CLASS}'] [class*='${CHAT_SELECTORS.SETTINGS_PANEL_CLASS}']`);
      if (settingsPanel && !settingsPanel.querySelector(".better-chat-found")) {
        settingsPanel.classList.add("better-chat-found");
        ScriptEventHandler.onSettingsPanelLoad(settingsPanel);
      }
      const tabSelector = findByPartialClass(chatList, CHAT_SELECTORS.PEOPLE_PANEL_TABS_WRAPPER_CLASS);
      const tabContent = findByPartialClass(chatList, CHAT_SELECTORS.PEOPLE_PANEL_TAB_CONTENT_CLASS);
      if (tabContent) {
        new MutationObserver((mutations) => {
          const hasRemovedLoader = mutations.flatMap((mutation) => [...mutation.removedNodes]).filter(isElement).map((node) => node.getAttribute("class")).filter(notNull).find((className) => className.includes(CHAT_SELECTORS.PEOPLE_PANEL_LOADING));
          if (!hasRemovedLoader)
            return;
          ScriptEventHandler.handlePanelTab(tabSelector, tabContent);
        }).observe(tabContent, { childList: true });
        new Promise(async (resolve, reject) => {
          let times = 0;
          let element;
          do {
            element = findByPartialClass(document, CHAT_SELECTORS.PEOPLE_PANEL_TAB_ACTIVE_CLASS);
            if (!element) {
              await sleep(100);
            }
          } while (!element && ++times < 1e3);
          if (element)
            resolve();
          else
            reject();
        }).then(() => {
          ScriptEventHandler.handlePanelTab(
            findByPartialClass(chatList, CHAT_SELECTORS.PEOPLE_PANEL_TABS_WRAPPER_CLASS),
            findByPartialClass(chatList, CHAT_SELECTORS.PEOPLE_PANEL_TAB_CONTENT_CLASS)
          );
        });
      }
    }
    static handlePanelTab(tabSelector, tabContent) {
      const activeTab = findByPartialClass(tabSelector, CHAT_SELECTORS.PEOPLE_PANEL_TAB_ACTIVE_CLASS).textContent.toLowerCase();
      if (activeTab !== "chats") {
        ScriptEventHandler.onPeopleListLoad(tabContent);
      }
    }
    static onUnreadCountChange() {
      AccessibilityFeature.describePeople();
      AccessibilityFeature.describeUnreadChats();
    }
  }
  class StylingFeature {
    static 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);
      }
    }
    static defaultStyles() {
      StylingFeature.includeStyle(`
            #better-chat-settings-icon {
                align-self: center;
            }

            #better-chat-settings-icon button {
                color: #f7f7f7;
            }

            .better-chat-settings-overlay {
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                background-color: rgba(0, 0, 0, 0.5);
                bottom: 0;
                z-index: 1000;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .better-chat-settings-popup {
                width: 300px;
                min-height: 300px;
                padding: 4px;
                overflow: auto;
                max-height: 100vh;
            }
            
            body:not(.dark-mode) .better-chat-settings-popup {
                background-color: #f7f7f7;
            }
            
            body.dark-mode .better-chat-settings-popup {
                background-color: #414141;
            }

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

            body.dark-mode .better-chat-settings-popup button {
                color: #ddd;
            }

            .better-chat-settings-highlight-entry {
                display: flex;
                gap: 4px;
            }
            
            [class*='${CHAT_SELECTORS.SETTINGS_PANEL_HEADER_CLASS}'] {
                justify-content: initial;
            }
            
            [class*='${CHAT_SELECTORS.SETTINGS_PANEL_HEADER_TITLE_CLASS}'] {
                flex-grow: 1;
            }
        `);
    }
    static improveStyles() {
      StylingFeature.includeStyle(`
            [class*='${CHAT_SELECTORS.MESSAGE_BODY_CLASS}'] {
                cursor: initial !important;
            }
        `);
    }
    static hideAvatars() {
      if (settings.messages.hideAvatars) {
        StylingFeature.includeStyle(`
                [class*='${CHAT_SELECTORS.MESSAGE_AVATAR_CLASS}'] {
                    display: none;
                }
            `);
      }
      if (settings.box.hideAvatars) {
        StylingFeature.includeStyle(`
                [class*='${CHAT_SELECTORS.CHAT_HEADER_AVATAR_CLASS}'] [class*='${CHAT_SELECTORS.AVATAR_WRAPPER_CLASS}'] > img,
                [class*='${CHAT_SELECTORS.MESSAGE_MINIMIZED_BOX_AVATAR_CLASS}'] [class*='${CHAT_SELECTORS.AVATAR_WRAPPER_CLASS}'] > img {
                    display: none;
                }
            `);
      }
    }
    static compact() {
      if (!settings.messages.compact)
        return;
      StylingFeature.includeStyle(`
            [class*='${CHAT_SELECTORS.MESSAGE_BODY_WRAPPER_CLASS}'] {
                margin-bottom: 0px !important;
            }

            [class*='${CHAT_SELECTORS.CHAT_BODY_CLASS}'] > div:last-child {
                margin-bottom: 8px !important;
            }
        `);
    }
    static groupRight() {
      if (!settings.box.groupRight)
        return;
      StylingFeature.includeStyle(`
            [class*='${CHAT_SELECTORS.CHAT_WRAPPER_WRAPPER_CLASS}'] {
                gap: 3px;
            }

            [class*='${CHAT_SELECTORS.CHAT_WRAPPER_CLASS}'] {
                margin-right: 0 !important;
            }
        `);
    }
    static leftAlignedText() {
      if (!settings.messages.leftAlignedText)
        return;
      StylingFeature.includeStyle(`
            [class*='${CHAT_SELECTORS.MESSAGE_SENDER_CLASS}'] {
                display: unset !important;
                font-weight: 700;
            }

            [class*='${CHAT_SELECTORS.MESSAGE_BODY_CLASS}'] [class*='${CHAT_SELECTORS.MESSAGE_SENDER_CLASS}'] {
                margin-right: 4px;
            }

            [class*='${CHAT_SELECTORS.MESSAGE_BODY_CLASS}'],
            [class*='${CHAT_SELECTORS.MESSAGE_BODY_SELF_CLASS}'] {
                background: none !important;
                border-radius: none !important;
                color: initial !important;
                padding: 0 !important;
            }

            [class*='${CHAT_SELECTORS.MESSAGE_BODY_WRAPPER_SELF_CLASS}'] {
                justify-content: normal !important;
            }

            [class*='${CHAT_SELECTORS.MESSAGE_CONTENT_WRAPPER_CLASS}'] {
                color: var(--chat-text-color) !important;
            }
        `);
    }
    static fontSize() {
      if (!settings.messages.fontSize.enabled)
        return;
      const { size } = settings.messages.fontSize;
      StylingFeature.includeStyle(`
            [class*='${CHAT_SELECTORS.MESSAGE_CONTENT_WRAPPER_CLASS}'],
            [class*='${CHAT_SELECTORS.MESSAGE_SENDER_CLASS}'] {
                font-size: ${size}px !important;
            }
            
            #${CHAT_SELECTORS.CHAT_ROOT_ID} {
                --torntools-chat-font-size: ${size}px;
            }
        `);
    }
    static mobileGroups() {
      if (!settings.box.mobileGroups)
        return;
      StylingFeature.includeStyle(`
            [class*='${CHAT_SELECTORS.GROUP_MENU_MOBILE_CLASS}'] {
                display: none !important;
            }
            
            [class*='${CHAT_SELECTORS.GROUP_MENU_DESKTOP_CLASS}'] {
                display: flex !important;
            }
        `);
    }
  }
  class AccessibilityFeature {
    static describeChatButtons(chat) {
      if (!settings.accessibility.describeButtons)
        return;
      findAll(chat, "button:not(.better-chat-described), *[role='button'][tabindex]").forEach((button) => AccessibilityFeature.describeChatButton(button));
    }
    static describeChatButton(button) {
      let description;
      const svg = button.querySelector("svg");
      if (svg) {
        const className = svg.getAttribute("class") || "";
        if (className.includes(CHAT_SELECTORS.CHAT_HEADER_MINIMIZE_ICON_CLASS)) {
          description = "Minimize this chat";
        } else if (className.includes(CHAT_SELECTORS.CHAT_HEADER_CLOSE_ICON_CLASS)) {
          description = "Close this chat";
        }
      }
      if (!description) {
        const className = button.getAttribute("class");
        if (className.includes(CHAT_SELECTORS.CHAT_HEADER_CLASS)) {
          description = false;
        } else if (className.includes(CHAT_SELECTORS.MESSAGE_SEND_BUTTON_CLASS)) {
          description = "Send your message";
        } else if (className.includes(CHAT_SELECTORS.CHAT_HEADER_INFO_BUTTON_CLASS)) {
          description = "Open possible actions";
        } else if (className.includes(CHAT_SELECTORS.CHAT_HEADER_INFO_CLASS)) {
          description = false;
        }
      }
      if (description)
        button.ariaLabel = description;
      else if (description === false)
        ;
      else
        console.warn("[Better Chat] Failed to describe this button.", button);
      button.classList.add("better-chat-described");
    }
    static appPanelAccessibility(panel) {
      findAllByPartialClass(panel, CHAT_SELECTORS.PEOPLE_PANEL_HEADER_BUTTON).forEach((button) => AccessibilityFeature.describeAppPanelButton(button));
    }
    static describeAppPanelButton(button) {
      let description;
      if (button.querySelector(`#${CHAT_SELECTORS.PEOPLE_PANEL_SETTINGS_BUTTON_ID}`)) {
        description = "Open chat settings";
      } else if (button.querySelector(`#${CHAT_SELECTORS.PEOPLE_PANEL_CLOSE_BUTTON_ID}`)) {
        description = "Close chat settings";
      } else
        console.warn("[Better Chat] Failed to describe this app panel button.", button);
      button.ariaLabel = description ?? null;
    }
    static describeMessageButtons(message, senderName) {
      if (!settings.accessibility.describeButtons)
        return;
      const senderElement = findByPartialClass(message, CHAT_SELECTORS.MESSAGE_SENDER_CLASS);
      if (settings.accessibility.presentationSender)
        ;
      else if (settings.accessibility.describeButtons) {
        senderElement.ariaLabel = `Open ${senderName}'s profile`;
      }
    }
    static describeRootPanels() {
      if (!settings.accessibility.describeButtons)
        return;
      findByPartialClassDelayed(document, CHAT_SELECTORS.CHAT_LIST_NOTEPAD_CLASS).then((notepadElement) => notepadElement.ariaLabel = "Open your notepad").catch((reason) => console.warn("[Better Chat] Failed to describe the notepad button.", reason));
      findByPartialClassDelayed(document, CHAT_SELECTORS.CHAT_LIST_SETTINGS_CLASS).then((settingsElement) => settingsElement.ariaLabel = "Open the chat settings").catch((reason) => console.warn("[Better Chat] Failed to describe the settings button.", reason));
      const mobileMenuElement = findByPartialClass(document, CHAT_SELECTORS.GROUP_MENU_MOBILE_CLASS);
      const mobileButtonElements = findAllByPartialClass(mobileMenuElement, CHAT_SELECTORS.GROUP_MENU_MOBILE_BUTTON_CLASS);
      mobileButtonElements.forEach((button, index) => {
        let description;
        if (index === 0) {
          description = "Faction chat";
        } else if (index === 1) {
          description = "Grouped chats";
        } else
          return;
        button.ariaLabel = description;
      });
    }
    static describePeople() {
      var _a;
      if (!settings.accessibility.describeButtons)
        return;
      const peopleElement = findByPartialClass(document, CHAT_SELECTORS.CHAT_LIST_PEOPLE_CLASS);
      const unreadMessages = parseInt(((_a = findByPartialClass(peopleElement, CHAT_SELECTORS.CHAT_MESSAGE_COUNT_CLASS)) == null ? void 0 : _a.textContent) || "0", 10);
      peopleElement.ariaLabel = `List all people | ${pluralize("unread message", unreadMessages)}`;
    }
    static describeUnreadChats() {
      if (!settings.accessibility.describeButtons)
        return;
      findAllByPartialClass(document, CHAT_SELECTORS.CHAT_LIST_GROUP_CHAT_CLASS).forEach((group) => {
        var _a;
        const unreadMessages = parseInt(((_a = findByPartialClass(group, CHAT_SELECTORS.CHAT_MESSAGE_COUNT_CLASS)) == null ? void 0 : _a.textContent) || "0", 10);
        const chatName = group.dataset.name || group.getAttribute("title") || "oops broken, please report";
        if (!group.dataset.name)
          group.dataset.name = chatName;
        if (unreadMessages > 0) {
          group.ariaLabel = `${chatName} | ${pluralize("unread message", unreadMessages)}`;
          group.removeAttribute("title");
        } else {
          group.ariaLabel = null;
          group.setAttribute("title", chatName);
        }
      });
    }
  }
  const _ChatGroupFeature = class _ChatGroupFeature {
    static moveGroupRight(chat) {
      if (!settings.box.groupRight)
        return;
      const avatarElement = findByPartialClass(chat, CHAT_SELECTORS.CHAT_HEADER_AVATAR_CLASS, "> *");
      const isGroup = avatarElement.tagName.toLowerCase() === "svg";
      if (isGroup) {
        chat.style.order = "1";
      }
    }
    static nameAutocompletion(chat) {
      if (!settings.box.nameAutocomplete)
        return;
      const avatarElement = findByPartialClass(chat, CHAT_SELECTORS.CHAT_HEADER_AVATAR_CLASS, "> *");
      const isGroup = avatarElement.tagName.toLowerCase() === "svg";
      if (!isGroup)
        return;
      const textarea = chat.querySelector("textarea");
      textarea.addEventListener("keydown", (event) => {
        if (event.code !== "Tab") {
          _ChatGroupFeature.currentUsername = null;
          _ChatGroupFeature.currentSearchValue = null;
          return;
        }
        event.preventDefault();
        const valueBeforeCursor = textarea.value.substring(0, textarea.selectionStart);
        const searchValueMatch = valueBeforeCursor.match(/([^\w-]?)([\w-]*)$/);
        if (_ChatGroupFeature.currentSearchValue === null)
          _ChatGroupFeature.currentSearchValue = searchValueMatch[2].toLowerCase();
        const matchedUsernames = findAllByPartialClass(chat, CHAT_SELECTORS.MESSAGE_BODY_CLASS, "button a").map((message) => message.textContent.substring(0, message.textContent.length - 1)).filter(
          (username, index2, array) => array.indexOf(username) === index2 && username.toLowerCase().startsWith(_ChatGroupFeature.currentSearchValue || "")
        ).sort();
        if (!matchedUsernames.length)
          return;
        let index = _ChatGroupFeature.currentUsername !== null ? matchedUsernames.indexOf(_ChatGroupFeature.currentUsername) + 1 : 0;
        if (index > matchedUsernames.length - 1)
          index = 0;
        _ChatGroupFeature.currentUsername = matchedUsernames[index];
        const valueStart = (searchValueMatch.index || 0) + searchValueMatch[1].length;
        textarea.value = textarea.value.substring(0, valueStart) + _ChatGroupFeature.currentUsername + textarea.value.substring(valueBeforeCursor.length, textarea.value.length);
        const selectionIndex = valueStart + _ChatGroupFeature.currentUsername.length;
        textarea.setSelectionRange(selectionIndex, selectionIndex);
      });
    }
  };
  __publicField(_ChatGroupFeature, "currentUsername", null);
  __publicField(_ChatGroupFeature, "currentSearchValue", null);
  let ChatGroupFeature = _ChatGroupFeature;
  class MessageFeature {
    static highlightMessages(message, senderName) {
      if (!settings.messages.highlight.length)
        return;
      const highlights = MessageFeature.buildHighlights();
      MessageFeature.nameHighlight(message, highlights, senderName);
      MessageFeature.messageHighlight(message, highlights);
    }
    static buildHighlights() {
      return settings.messages.highlight.map(({ search, color }) => ({
        search: search.replaceAll("%player%", getCurrentPlayerName()),
        color: convertColor(color)
      }));
    }
    static nameHighlight(message, highlights, senderName) {
      const nameHighlight = highlights.find(({ search }) => senderName.toLowerCase() === search.toLowerCase());
      if (!nameHighlight)
        return;
      const senderElement = findByPartialClass(message, CHAT_SELECTORS.MESSAGE_SENDER_CLASS);
      senderElement.setAttribute("style", `background-color: ${nameHighlight.color} !important;`);
    }
    static messageHighlight(message, highlights) {
      const messageElement = findByPartialClass(message, CHAT_SELECTORS.MESSAGE_CONTENT_WRAPPER_CLASS);
      const messageHighlight = highlights.find(({ search }) => messageElement.textContent.toLowerCase().includes(search.toLowerCase()));
      if (!messageHighlight)
        return;
      const wrapperElement = findByPartialClass(message, CHAT_SELECTORS.MESSAGE_BODY_WRAPPER_CLASS);
      wrapperElement.setAttribute("style", `background-color: ${messageHighlight.color} !important;`);
    }
  }
  class PeopleStatusFeature {
    static sortOnStatus(list) {
      if (!settings.people.sortOnStatus)
        return;
      list.querySelectorAll(`:scope > [class*='${CHAT_SELECTORS.PEOPLE_PANEL_MEMBER_CARD_CLASS}']`).forEach((card) => {
        let order;
        if (findByPartialClass(card, CHAT_SELECTORS.PEOPLE_PANEL_STATUS_ONLINE_CLASS)) {
          order = "0";
        } else if (findByPartialClass(card, CHAT_SELECTORS.PEOPLE_PANEL_STATUS_IDLE_CLASS)) {
          order = "1";
        } else if (findByPartialClass(card, CHAT_SELECTORS.PEOPLE_PANEL_STATUS_OFFLINE_CLASS)) {
          order = "2";
        } else
          return;
        card.style.order = order;
      });
    }
  }
  (() => ScriptEventHandler.onLoad())();

})();

QingJ © 2025

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