TORN: Better Chat

Improvements to the usability of chats 2.0.

您需要先安裝使用者腳本管理器擴展,如 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
// @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())();

})();