Nitro Type - Safe Space

Replaces the Race Track with a Typing Test Lobby Chatroom. Choose which users to mute and block, it's your "safe space".

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Nitro Type - Safe Space
// @version      0.6.1
// @description  Replaces the Race Track with a Typing Test Lobby Chatroom. Choose which users to mute and block, it's your "safe space".
// @author       Toonidy
// @match        *://*.nitrotype.com/race
// @match        *://*.nitrotype.com/race/*
// @match        *://*.nitrotype.com/profile
// @icon         https://i.ibb.co/YRs06pc/toonidy-userscript.png
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/dexie/3.2.1/dexie.min.js#sha512-ybuxSW2YL5rQG/JjACOUKLiosgV80VUfJWs4dOpmSWZEGwdfdsy2ldvDSQ806dDXGmg9j/csNycIbqsrcqW6tQ==
// @require      https://greasyfork.org/scripts/443718-nitro-type-userscript-utils/code/Nitro%20Type%20Userscript%20Utils.js?version=1042360
// @license      MIT
// @namespace    https://greasyfork.org/users/858426
// ==/UserScript==
 
/* globals Dexie findReact createLogger */
 
const logging = createLogger("Nitro Type Safe Space")
 
// Config storage
const db = new Dexie("NTSafeSpace")
db.version(1).stores({
    users: "id, &username, team, displayName, status",
})
db.open().catch(function (e) {
    logging.error("Init")("Failed to open up the config database", e)
})
 
/////////////////////
//  Settings Page  //
/////////////////////
 
if (window.location.pathname === "/profile") {
    //////////////////
    //  Components  //
    //////////////////
 
    const safeSpaceSettingRoot = document.createElement("div")
    safeSpaceSettingRoot.classList.add("g-b", "g-b--9of12")
    safeSpaceSettingRoot.innerHTML = `
        <h2 class="tbs">Nitro Type Safe Space Settings</h2>
        <p class="tc-ts">Manage settings from this Userscript.</p>
        <p class="input-label">Mute/Blocked Users<p>
        <table class="table table--selectable table--striped">
            <thead class="table-head">
                <tr class="table-row">
                    <th scope="col" class="table-cell table-cell--racer">Racer</th>
                    <th scope="col" class="table-cell table-cell--status">Status</th>
                    <th scope="col" class="table-cell table-cell--remove" style="width: 90px">Remove?</th>
                </tr>
            </thead>
            <tbody class="table-body">
            </tbody>
        </table>`
 
    const userTableBody = safeSpaceSettingRoot.querySelector("tbody.table-body")
 
    const userRow = document.createElement("tr")
    userRow.classList.add("table-row")
    userRow.innerHTML = `
        <td class="table-cell table-cell--racer">
            <div class="bucket bucket--s bucket--c">
                <div class="bucket-media bucket-media--w90">
                    <img class="img--noMax db">
                </div>
                <div class="bucket-content">
                    <div class="df df--align-center">
                        <div class="prxxs"><img alt="Nitro Gold" class="icon icon-nt-gold-s" src="/dist/site/images/themes/profiles/gold/nt-gold-icon-xl.png"></div>
                        <div class="prxxs df df--align-center">
                            <a class="link link--bare mrxxs twb" style="color: rgb(253, 182, 77);"></a>
                            <span class="type-ellip type-gold tss"></span>
                        </div>
                    </div>
                    <div class="tsi tc-lemon tsxs"></div>
                </div>
            </div>
        </td>
        <td class="table-cell table-cell--status">
            <select class="input-select">
                <option value="MUTE">Muted</option>
                <option value="BLOCK">Blocked</option>
            </select>
        </td>
        <td class="table-cell table-cell--remove tar prs">
            <button title="Remove Block/Mute User" type="button" class="btn btn--negative">
                <svg class="icon icon-x--s"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-x"></use></svg>
            </button>
        </td>`
 
    const handleRowClick = (e) => {
        const row = e.target.closest(".table-row"),
            input = e.target.closest("a, button, select"),
            userID = row && !input ? parseInt(row.dataset.user, 10) : null
        if (userID !== null && !isNaN(userID)) {
            db.users.get(userID).then((user) => {
                window.location.href = `/racer/${user.username}`
            })
        }
    }
 
    const handleStatusUpdateChange = (e) => {
        const targetElement = e.target.closest("select"),
            row = e.target.closest(".table-row"),
            userID = row ? parseInt(row.dataset.user, 10) : null
        if (userID !== null && !isNaN(userID)) {
            db.users.update(userID, { status: targetElement.value })
        }
    }
 
    const handleRemoveButtonClick = (e) => {
        const row = e.target.closest(".table-row"),
            userID = row ? parseInt(row.dataset.user, 10) : null
        if (userID !== null && !isNaN(userID)) {
            db.users.delete(userID).then(() => row.remove())
        }
    }
 
    db.users.count().then((total) => {
        if (total === 0) {
            const emptyRow = document.createElement("tr")
            emptyRow.classList.add("table-row")
            emptyRow.innerHTML = `<td class="table-cell" colspan="3">No racers found</td>`
            userTableBody.append(emptyRow)
            userTableBody.parentNode.classList.remove("table--selectable")
            return
        }
 
        const rowFragment = document.createDocumentFragment()
        db.users
            .each((userData) => {
                const row = userRow.cloneNode(true),
                    carImage = row.querySelector("img.img--noMax"),
                    teamLink = row.querySelector("a.link"),
                    racerName = row.querySelector(".type-ellip"),
                    statusSelect = row.querySelector("select"),
                    removeButton = row.querySelector("button"),
                    displayName = userData.displayName || userData.username
 
                row.dataset.user = userData.id
                row.addEventListener("click", handleRowClick)
 
                carImage.src = userData.carImgSrc
                carImage.alt = `${displayName}'s car`
 
                teamLink.parentNode.title = displayName
                racerName.textContent = `${userData.team ? " " : ""}${displayName}`
                row.querySelector(".tsi").textContent = `"${userData.title}"`
 
                if (!userData.team) {
                    teamLink.remove()
                } else {
                    teamLink.textContent = `[${userData.team}]`
                    teamLink.href = `/team/${userData.team}`
                    teamLink.style.color = `#${userData.teamColor}`
                }
 
                if (!userData.isGold) {
                    row.querySelector(".icon-nt-gold-s").parentNode.remove()
                    racerName.classList.remove("type-gold")
                }
 
                statusSelect.value = userData.status
                statusSelect.addEventListener("change", handleStatusUpdateChange)
 
                removeButton.addEventListener("click", handleRemoveButtonClick)
 
                rowFragment.append(row)
            })
            .then(() => {
                userTableBody.append(rowFragment)
            })
    })
 
    /////////////
    //  Final  //
    /////////////
 
    /** Mutation observer to check whether setting page has loaded. */
    const settingPageObserver = new MutationObserver(([mutation], observer) => {
        const sideMenu = mutation.target.querySelector(".has-btn"),
            originalSettingRoot = mutation.target.querySelector(".g-b.g-b--9of12")
        if (sideMenu && originalSettingRoot) {
            observer.disconnect()
 
            const menuSafeSpaceButton = document.createElement("button")
            menuSafeSpaceButton.classList.add("btn", "btn--fw")
            menuSafeSpaceButton.textContent = "Nitro Type Safe Space"
            menuSafeSpaceButton.addEventListener("click", () => {
                const currentActiveButton = sideMenu.querySelector(".btn.is-active")
                if (currentActiveButton) {
                    currentActiveButton.classList.remove("is-active")
                }
                menuSafeSpaceButton.classList.add("is-active")
                originalSettingRoot.replaceWith(safeSpaceSettingRoot)
            })
 
            const handleOriginalMenuButtonClick = () => {
                menuSafeSpaceButton.classList.remove("is-active")
                safeSpaceSettingRoot.replaceWith(originalSettingRoot)
            }
            sideMenu.querySelectorAll(".btn").forEach((node) => {
                node.addEventListener("click", handleOriginalMenuButtonClick)
            })
 
            sideMenu.append(menuSafeSpaceButton)
        }
    })
    settingPageObserver.observe(document.querySelector("main.structure-content"), { childList: true })
 
    return
}
 
///////////////////
//  Racing Page  //
///////////////////
 
if (window.location.pathname === "/race" || window.location.pathname.startsWith("/race/")) {
    const raceContainer = document.getElementById("raceContainer"),
        canvasTrack = raceContainer?.querySelector("canvas"),
        raceObj = raceContainer ? findReact(raceContainer) : null
    if (!raceContainer || !canvasTrack || !raceObj) {
        logging.error("Init")("Could not find the race track")
        return
    }
    if (!raceObj.props.user.loggedIn) {
        logging.error("Init")("Safe Space is not available for Guest Racing")
        return
    }
 
    //////////////
    //  Styles  //
    //////////////
 
    const style = document.createElement("style")
    style.appendChild(
        document.createTextNode(`
:root {
    --chat-contacts-width: 265px;
}
 
.nt-safe-space-root {
    position: relative;
    box-sizing: border-box;
    width: 1024px;
    height: 400px;
    background-color: #202020;
}
 
/* Some Overrides */
.race-results {
    z-index: 6;
}
 
/* Info Section */
.nt-safe-space-info {
    position: absolute;
    left: 8px;
    top: 8px;
    bottom: 8px;
    right: 633px;
    display: flex;
    flex-direction: column;
    border-radius: 8px;
    color: #eee;
    background-color: #303030;
    transition: 0.3s right ease;
}
.nt-safe-space-chat-contacts-hidden .nt-safe-space-info {
    right: calc(617px - var(--chat-contacts-width));
}
.nt-safe-space-info-status {
    display: flex;
    flex-direction: column;
    flex-grow: 1;
    justify-content: center;
    align-items: center;
}
.nt-safe-space-info-status-title {
    font-size: 24px;
    font-weight: 600;
    text-align: center;
    margin-bottom: 14px;
}
.nt-safe-space-info-status-subtitle {
    font-size: 14px;
    text-align: center;
}
.nt-safe-space-info-status-wampus {
    display: flex;
    justify-content: center;
    margin-top: 1rem;
}
.nt-safe-space-info-status-wampus img {
    width: 100px;
    height: 64px;
}
.nt-safe-space-info-footer {
    position: relative;
    height: 138px;
    border-bottom-left-radius: 8px;
    border-bottom-right-radius: 8px;
}
.nt-safe-space-info-footer .nt-safe-space-contact-item {
    position: absolute;
    right: 8px;
    bottom: 8px;
    padding: 8px;
    border-radius: 8px;
}
 
/* Chat */
.nt-safe-space-chat {
    position: absolute;
    left: 400px;
    right: 8px;
    top: 8px;
    bottom: 8px;
    z-index: 5;
    display: flex;
    border-radius: 8px;
    overflow: hidden;
    transition: 0.3s left ease;
}
.nt-safe-space-chat-contacts-hidden .nt-safe-space-chat {
    left: calc(415px + var(--chat-contacts-width));
}
 
/* Chat Contacts */
.nt-safe-space-contacts {
    display: flex;
    flex-direction: column;
    width: var(--chat-contacts-width);
    border-top-left-radius: 8px;
    border-bottom-left-radius: 8px;
    border-right-width: 1px;
    border-right-style: solid;
    border-right-color: #34344a;
    background-color: #0b0b10;
    color: #fff;
    transition-duration: 0.3s;
    transition-property: width, border-right-width;
    transition-timing-function: ease;
    overflow: hidden;
}
.nt-safe-space-chat-contacts-hidden .nt-safe-space-contacts {
    border-right-width: 0px;
    width: 0px;
}
.nt-safe-space-contact-item {
    padding: 2px 8px;
    border-bottom: 1px solid #20202e;
    background-color: #111218;
}
.nt-safe-space-contact-item:hover {
    background-color: #181822;
}
.nt-safe-space-contact-item:first-of-type {
    padding-top: 8px;
}
.nt-safe-space-contact-item:nth-child(4) {
    padding-bottom: 8px;
    border-bottom: 0;
}
.nt-safe-space-contact-item.alt-row {
    background-color: #181a22;
}
.nt-safe-space-contact-item.alt-row:hover {
    background-color: #20212c;
}
.nt-safe-space-contact-item-body {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.nt-safe-space-contact-player {
    display: flex;
    align-items: center;
    flex-grow: 1;
}
.nt-safe-space-contact-avatar  {
    display: flex;
    width: 64px;
    height: 64px;
    overflow: hidden;
    margin-right: 4px;
}
.nt-safe-space-contact-avatar img {
    margin: auto;
    max-width: 100%;
    max-height: 100%;
}
.nt-safe-space-contact-speech-bubble {
    position: relative;
    background: #fff;
    border-radius: 8px;
    padding: 4px;
    margin-left: 10px;
    transition: opacity 0.2s ease;
    opacity: 1;
}
.nt-safe-space-contact-speech-bubble.nt-safe-space-hidden {
    opacity: 0;
}
.nt-safe-space-contact-speech-bubble:after {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    width: 0;
    height: 0;
    border: 10px solid transparent;
    border-right-color: #fff;
    border-left: 0;
    margin-top: -10px;
    margin-left: -10px;
}
.nt-safe-space-contact-speech-bubble-img {
    background-repeat: no-repeat;
    background-size: contain;
    background-position: center;
    width: 48px;
    height: 48px;
}
.nt-safe-space-contact-item-name {
    display: flex;
    align-items: center;
    font-size: 12px;
    font-weight: 600;
    margin-bottom: 4px;
}
.nt-safe-space-contact-menu {
    display: flex;
    flex-direction: column;
    font-size: 10px;
}
.nt-safe-space-contact-menu-item {
    display: flex;
    align-items: center;
    padding: 4px;
    margin-bottom: 2px;
    border-radius: 4px;
    width: 80px;
    cursor: pointer;
}
.nt-safe-space-contact-menu-item:hover {
    background-color: rgba(255, 255, 255, 0.1);
}
.nt-safe-space-contact-menu-icon {
    margin-right: 8px;
}
 
/* Chat Messages Container */
.nt-safe-space-chatroom {
    flex-grow: 1;
    background-color: #20222e;
    background-image: url(/dist/site/images/backgrounds/bg-noise.png)
}
.nt-safe-space-chatroom-messages {
    position: relative;
    height: 210px;
    transition: height 0.2s ease;
}
.nt-safe-space-chatroom.hide-reply-options .nt-safe-space-chatroom-messages {
    height: 344px;
}
.nt-safe-space-chatroom.disable-reply .nt-safe-space-chatroom-messages {
    height: 384px;
}
.nt-safe-space-chatroom-messages-scrollable {
    position: absolute;
    left: 8px;
    right: 8px;
    top: 8px;
    bottom: 8px;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    scrollbar-face-color: #fff;
    scrollbar-track-color: #000;
    color: #eee;
    font-size: 12px;
}
.nt-safe-space-chatroom-messages-scrollable::-webkit-scrollbar {
    width: 4px;
    height: 4px;
}
.nt-safe-space-chatroom-messages-scrollable::-webkit-scrollbar-thumb {
    background: #fff;
}
.nt-safe-space-chatroom-messages-scrollable::-webkit-scrollbar-track {
    background: #000;
}
 
/* Chat Message Item */
.nt-safe-space-chatroom-message {
    margin-top: auto;
    margin-bottom: 16px;
}
.nt-safe-space-chatroom-message:last-of-type {
    margin-bottom: unset;
}
.nt-safe-space-chatroom-message-heading, .nt-safe-space-chatroom-message-body  {
    display: flex;
    align-items: center;
}
.nt-safe-space-chatroom-message-heading {
    margin-bottom: 4px;
    font-weight: 600;
}
.nt-safe-space-chatroom-message-body {
    display: inline-flex;
    border-radius: 8px;
    padding-top: 4px;
    padding-bottom: 4px;
    padding-left: 8px;
    padding-right: 8px;
    background-color: rgba(255, 255, 255, 0.1);
}
.nt-safe-space-chatroom-message-team,
.nt-safe-space-chatroom-message-name {
    margin-right: 0.5ch;
}
.nt-safe-space-chatroom-message-name.nt-gold-user,
.nt-safe-space-contact-item-name.nt-gold-user {
    color: #E0BB2F;
}
.nt-safe-space-chatroom-message-heading svg.icon,
.nt-safe-space-contact-item svg.icon {
    margin-right: 0.2ch;
}
.nt-safe-space-chatroom-message-body .nt-safe-space-chatroom-message-text.system-message {
    font-style: italic;
}
.nt-safe-space-chatroom-message-body .nt-safe-space-chatroom-mesasge-img {
    background-repeat: no-repeat;
    background-size: contain;
    background-position: center;
    width: 48px;
    height: 48px;
    margin-left: 1ch;
}
.nt-safe-space-chatroom-message-time {
    font-size: 10px;
    margin-top: 2px;
}
.nt-safe-space-chatroom-message.is-me {
    display: flex;
    flex-direction: column;
}
.nt-safe-space-chatroom-message.is-me,
.nt-safe-space-chatroom-message.is-me .nt-safe-space-chatroom-message-heading,
.nt-safe-space-chatroom-message.is-me .nt-safe-space-chatroom-message-body {
    margin-left: auto;
}
.nt-safe-space-chatroom-message.is-me .nt-safe-space-chatroom-message-time {
    text-align: right;
}
 
/* Chat Reply */
.nt-safe-space-chatroom-reply {
    height: 176px;
}
.nt-safe-space-chatroom-reply-toolbar {
    background-color: #093c60;
    padding: 2px;
}
.nt-safe-space-chatroom-reply-toolbar.friend-race {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
}
.nt-safe-space-chatroom-reply-toolbar-option {
    position: relative;
    padding: 8px;
    width: 100%;
    color: #fff;
    transition: background-color 0.2s ease;
    background-color: rgba(0, 0, 0, 0.1);
}
.nt-safe-space-chatroom-reply-toolbar-option:hover {
    background-color: rgba(0, 0, 0, 0.2);
}
.nt-safe-space-chatroom-reply-toolbar-option.selected {
    background-color: rgba(0, 0, 0, 0.3);
}
.nt-safe-space-chatroom.hide-reply-options .nt-safe-space-chatroom-reply-toolbar-option {
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
}
.nt-safe-space-chatroom-reply-toolbar-option svg {
    margin: 0 auto;
    width: 20px;
    height: 20px;
}
.nt-safe-space-chatroom-reply-options {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 2px;
    padding: 2px;
    background: linear-gradient(to bottom, #167ac3 30%, #1C99F4 100%);
}
.nt-safe-space-chatroom-reply-sticker {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
    padding: 8px;
    background-color: rgba(0, 0, 0, 0.3);
    transition: background-color 0.2s ease;
    cursor: pointer;
}
.nt-safe-space-chatroom-reply-sticker:hover{
    background-color: #eee;
}
.nt-safe-space-chatroom-reply-sticker.nt-space-space-activated {
    background-color: #fff;
}
.nt-safe-space-chatroom-reply-sticker-img {
    background-repeat: no-repeat;
    background-size: contain;
    background-position: center;
    transition: background-image 0.2s ease;
    width: 48px;
    height: 48px;
}
.nt-safe-space-chatroom-reply-sticker-shortcut, .nt-safe-space-chatroom-reply-toolbar-option-shortcut {
    position: absolute;
    right: 0;
    top: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-bottom-left-radius: 4px;
    background-color: rgba(0, 0, 0, 0.3);
    color: #fff;
    font-size: 12px;
}`)
    )
    document.head.appendChild(style)
 
    const root = document.createElement("div")
    root.classList.add("nt-safe-space-root", "nt-safe-space-chat-contacts-hidden")
 
    //////////////////
    //  Components  //
    //////////////////
 
    /** Display a chatroom with messages. */
    const ChatRoom = ((safeSpaceContainer, raceObj, db) => {
        const racerContactIDPrefix = "ntSafeSpaceRacer_",
            friendIconSVG = `<svg class="icon icon-friends-s tc-lemon"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-friends"></use></svg>`,
            smileyIconSVG = `<svg class="icon icon-smiley-l"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-smiley"></use></svg>`,
            chatIconSVG = `<svg class="icon icon-chat"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-chat"></use></svg>`,
            blockIconSVG = `<svg class="icon icon-lock-outline"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-lock-outline"></use></svg>`,
            muteIconSVG = `<svg class="icon icon-eye"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-eye"></use></svg>`,
            root = document.createElement("div"),
            systemUser = {
                userID: 0,
                profile: {
                    displayName: "Typing Test Instructor",
                    username: "Typing Test Instructor",
                    tag: null,
                    tagColor: null,
                    membership: "normal",
                },
            }
        let userStickers = [],
            userData = {},
            isStickers = true,
            isFriendRace = raceObj.state.friendsRace,
            racerCount = 0,
            userSpeechBubbleTimer = {},
            chatButtonTimer = [],
            updatingDB = [],
            raceKeyboardObj,
            originalChatObj
 
        root.classList.add("nt-safe-space-chat")
        root.innerHTML = `
            <div class="nt-safe-space-contacts"></div>
            <div class="nt-safe-space-chatroom">
                <div class="nt-safe-space-chatroom-messages">
                    <div class="nt-safe-space-chatroom-messages-scrollable"></div>
                </div>
                <div class="nt-safe-space-chatroom-reply">
                    <div class="nt-safe-space-chatroom-reply-toolbar friend-race">
                        <button class="nt-safe-space-chatroom-reply-toolbar-option option-sticker selected" type="button" title="Send Sticker">
                            ${smileyIconSVG}
                            <div class="nt-safe-space-chatroom-reply-toolbar-option-shortcut">S</div>
                        </button>
                        <button class="nt-safe-space-chatroom-reply-toolbar-option option-chat" type="button" title="Send Chat Message">
                            ${chatIconSVG}
                            <div class="nt-safe-space-chatroom-reply-toolbar-option-shortcut">C</div>
                        </button>
                    </div>
                    <div class="nt-safe-space-chatroom-reply-options">
                        ${Array.from(Array(8).keys())
                            .map(
                                (i) =>
                                    `<button class="nt-safe-space-chatroom-reply-sticker" type="button" data-stickerindex="${i}">
                            <div class="nt-safe-space-chatroom-reply-sticker-img"></div>
                            <div class="nt-safe-space-chatroom-reply-sticker-shortcut">${i + 1}</div>
                        </button>`
                            )
                            .join("")}
                    </div>
                </div>
            </div>`
 
        const handleNoHighlightButtonMouseDown = (e) => {
            e.preventDefault()
        }
 
        const handleChatOptionButtonClick = (e) => {
            e.preventDefault()
            const targetElement = e.target.closest(".nt-safe-space-chatroom-reply-toolbar-option")
            if (targetElement.classList.contains("selected")) {
                targetElement.classList.remove("selected")
                toggleChatOptions(false)
                return
            }
            toggleChatOptions(true)
            isStickers = targetElement.classList.contains("option-sticker")
            refreshChatOptions()
            root.querySelectorAll(".nt-safe-space-chatroom-reply-toolbar-option").forEach((optionElement) => {
                optionElement.classList.remove("selected")
            })
            targetElement.classList.add("selected")
        }
 
        const handleChatSendButtonClick = (e) => {
            e.preventDefault()
            const targetElement = e.target.closest(".nt-safe-space-chatroom-reply-sticker"),
                index = targetElement ? parseInt(targetElement.dataset.stickerindex, 10) : null
            if (index === null || isNaN(index)) {
                return
            }
            if (isStickers) {
                originalChatObj.sendMessage(userStickers[index].id, userStickers[index].src, "sticker")
            } else {
                originalChatObj.sendMessage(index, raceObj.props.chatTexts[index], "text")
            }
            flashChatButton(index)
        }
 
        root.querySelectorAll(".nt-safe-space-chatroom-reply-toolbar-option").forEach((node) => {
            node.addEventListener("click", handleChatOptionButtonClick)
            node.addEventListener("mousedown", handleNoHighlightButtonMouseDown)
        })
        root.querySelectorAll(".nt-safe-space-chatroom-reply-sticker").forEach((node) => {
            node.addEventListener("click", handleChatSendButtonClick)
            node.addEventListener("mousedown", handleNoHighlightButtonMouseDown)
        })
 
        const buttonSticker = root.querySelector(".nt-safe-space-chatroom-reply-toolbar-option.option-sticker"),
            buttonChat = root.querySelector(".nt-safe-space-chatroom-reply-toolbar-option.option-chat")
 
        if (!isFriendRace) {
            root.querySelector(".nt-safe-space-chatroom-reply-toolbar").classList.remove("friend-race")
            buttonChat.remove()
        }
 
        const chatMessages = root.querySelector(".nt-safe-space-chatroom-messages-scrollable")
 
        const refreshChatOptions = () => {
            root.querySelectorAll(".nt-safe-space-chatroom-reply-sticker-img").forEach((stickerItemContainer, i) => {
                if (isStickers) {
                    if (userStickers[i]) {
                        stickerItemContainer.parentNode.title = userStickers[i].name
                        stickerItemContainer.style.backgroundImage = `url(${userStickers[i].src})`
                    } else {
                        stickerItemContainer.parentNode.title = ""
                        stickerItemContainer.parentNode.style.display = "none"
                    }
                } else {
                    stickerItemContainer.style.backgroundImage = `url(/dist/site/images/chat/canned/chat_${i}.png)`
                    stickerItemContainer.parentNode.title = raceObj.props.chatTexts[i]
                    stickerItemContainer.parentNode.style.display = ""
                }
            })
        }
 
        const toggleChatOptions = (show) => {
            if (show) {
                chatMessages.parentNode.parentNode.classList.remove("hide-reply-options")
            } else {
                chatMessages.parentNode.parentNode.classList.add("hide-reply-options")
            }
        }
 
        const toggleChat = (show) => {
            if (show) {
                chatMessages.parentNode.parentNode.classList.remove("disable-reply")
            } else {
                chatMessages.parentNode.parentNode.classList.add("disable-reply")
            }
        }
 
        const flashChatButton = (index) => {
            const button = root.querySelector(`.nt-safe-space-chatroom-reply-sticker[data-stickerindex="${index}"]`)
            if (button) {
                if (chatButtonTimer[index]) {
                    clearTimeout(chatButtonTimer[index])
                }
                button.classList.add("nt-space-space-activated")
                chatButtonTimer[index] = setTimeout(() => {
                    button.classList.remove("nt-space-space-activated")
                }, 5e2)
            }
        }
 
        const addRacer = (user, status) => {
            const { userID } = user,
                { tag, tagColor, displayName, username, carID, carHueAngle } = user.profile,
                isMe = userID == currentUserID,
                isGold = user.profile.membership === "gold",
                isFriend = friendIDs && friendIDs.includes(userID) ? true : user.isFriend,
                imgCarSrc = raceObj.props.getCarUrl(carID, false, carHueAngle, false),
                newRacerElement = chatContactTemplate.cloneNode(true),
                newRacerTeamNode = newRacerElement.querySelector(".nt-safe-space-chatroom-message-team"),
                newRacerNameNode = newRacerElement.querySelector(".nt-safe-space-chatroom-message-name"),
                newRacerAvatarNode = newRacerElement.querySelector(".nt-safe-space-contact-avatar img"),
                newMuteButton = newRacerElement.querySelector(".nt-safe-space-btn-mute"),
                newBlockButton = newRacerElement.querySelector(".nt-safe-space-btn-block")
            newRacerElement.id = `${racerContactIDPrefix}${userID}`
            newRacerElement.dataset.user = userID
            newRacerNameNode.textContent = displayName || username
            newRacerAvatarNode.src = imgCarSrc
            newRacerAvatarNode.alt = `${displayName || username}'s car`
            if (isMe) {
                newRacerElement.classList.add("is-me")
                newRacerElement.querySelector(".nt-safe-space-contact-menu").remove()
            }
            if (tag) {
                newRacerTeamNode.textContent = `[${tag}]`
                newRacerTeamNode.style.color = `#${tagColor}`
            } else {
                newRacerTeamNode.remove()
            }
            if (!isGold) {
                newRacerNameNode.classList.remove("nt-gold-user")
                newRacerElement.querySelector(".icon-nt-gold-s").remove()
            }
            if (!isFriend) {
                newRacerElement.querySelector(".nt-safe-space-chatroom-message-friend").remove()
            }
            if (racerCount % 2 !== 0 || isMe) {
                newRacerElement.classList.add("alt-row")
            }
            if (status === "MUTE") {
                newMuteButton.classList.remove("nt-safe-space-btn-mute")
                newMuteButton.classList.add("nt-safe-space-btn-unmute")
                newMuteButton.querySelector(".nt-safe-space-contact-menu-label").textContent = "Unmute"
            }
            newMuteButton.addEventListener("click", handleContactOptionButtonClick)
            newBlockButton.addEventListener("click", handleContactOptionButtonClick)
            newMuteButton.addEventListener("mousedown", handleNoHighlightButtonMouseDown)
            newBlockButton.addEventListener("mousedown", handleNoHighlightButtonMouseDown)
 
            userData[userID] = user
 
            if (!isMe) {
                chatContacts.appendChild(newRacerElement)
                racerCount++
                safeSpaceContainer.classList.remove("nt-safe-space-chat-contacts-hidden")
            }
 
            return newRacerElement
        }
 
        const removeRacer = (userID) => {
            const contact = document.getElementById(`${racerContactIDPrefix}${userID}`)
            if (!contact || contact.classList.contains("is-me") || userID === currentUserID) {
                return
            }
            contact.remove()
 
            racerCount--
            if (racerCount === 0) {
                safeSpaceContainer.classList.add("nt-safe-space-chat-contacts-hidden")
            }
            root.querySelectorAll(".nt-safe-space-contact-item").forEach((node, i) => {
                if (i % 2 !== 0) {
                    node.classList.add("alt-row")
                } else {
                    node.classList.remove("alt-row")
                }
            })
        }
 
        const getRacer = (userID) => {
            return userData[userID]
        }
 
        const updateUser = (userID, status, user) => {
            if (updatingDB.includes(userID)) {
                return
            }
            user = user || raceObj.state.racers.find((r) => r.userID === userID)
            if (!user) {
                logging.warn("Chat")("User not found for sync", userID)
                return
            }
            const { tag, tagColor, displayName, username, title, membership, carID, carHueAngle } = user.profile,
                carImgSrc = raceObj.props.getCarUrl(carID, false, carHueAngle, false)
            updatingDB = updatingDB.concat(userID)
            return db.users
                .put({
                    id: userID,
                    username,
                    displayName,
                    isGold: membership === "gold",
                    title,
                    team: tag,
                    teamColor: tagColor,
                    carID: tagColor,
                    carHueAngle: tagColor,
                    carImgSrc,
                    status,
                })
                .then(() => {
                    updatingDB = updatingDB.filter((uid) => uid !== userID)
                    return true
                })
        }
 
        const muteUser = (userID) => {
            const user = raceObj.state.racers.find((r) => r.userID === userID)
            if (!user) {
                logging.warn("Chat")("Muting user not found", userID)
                return
            }
            updateUser(userID, "MUTE").then(() => {
                addMessage("system", user, "Has been muted =)")
                const muteButton = root.querySelector(`#${racerContactIDPrefix}${userID} .nt-safe-space-btn-mute`)
                muteButton.classList.remove("nt-safe-space-btn-mute")
                muteButton.classList.add("nt-safe-space-btn-unmute")
                muteButton.querySelector(".nt-safe-space-contact-menu-label").textContent = "Unmute"
            })
        }
 
        const unmuteUser = (userID) => {
            if (updatingDB.includes(userID)) {
                return
            }
            const user = raceObj.state.racers.find((r) => r.userID === userID)
            if (!user) {
                logging.warn("Chat")("Muting user not found", userID)
                return
            }
            updatingDB = updatingDB.concat(userID)
            db.users.delete(user.userID).then(() => {
                addMessage("system", user, "Has been unmuted =)")
                updatingDB = updatingDB.filter((uid) => uid !== userID)
                const muteButton = root.querySelector(`#${racerContactIDPrefix}${userID} .nt-safe-space-btn-unmute`)
                muteButton.classList.remove("nt-safe-space-btn-unmute")
                muteButton.classList.add("nt-safe-space-btn-mute")
                muteButton.querySelector(".nt-safe-space-contact-menu-label").textContent = "Mute"
            })
        }
 
        const blockUser = (userID) => {
            const user = raceObj.state.racers.find((r) => r.userID === userID)
            if (!user) {
                logging.warn("Chat")("Muting user not found", userID)
                return
            }
            updateUser(userID, "BLOCK").then(() => {
                addMessage("system", user, "Has been blocked =)")
                removeRacer(userID)
            })
        }
 
        // Chat Contact Template
        const chatContacts = root.querySelector(".nt-safe-space-contacts"),
            chatContactTemplate = document.createElement("div")
        chatContactTemplate.classList.add("nt-safe-space-contact-item")
        chatContactTemplate.innerHTML = `
            <div class="nt-safe-space-contact-item-name">
                <img class="icon icon-nt-gold-s" src="/dist/site/images/themes/profiles/gold/nt-gold-icon-xl.png" alt="Nitro Gold">
                <span class="nt-safe-space-chatroom-message-team"></span>
                <span class="nt-safe-space-chatroom-message-name nt-gold-user"></span>
                <span class="nt-safe-space-chatroom-message-friend">${friendIconSVG}</span>
            </div>
            <div class="nt-safe-space-contact-item-body">
                <div class="nt-safe-space-contact-player">
                    <div class="nt-safe-space-contact-avatar">
                        <img />
                    </div>
                    <div class="nt-safe-space-contact-speech-bubble nt-safe-space-hidden">
                        <div class="nt-safe-space-contact-speech-bubble-img"></div>
                    </div>
                </div>
                <div class="nt-safe-space-contact-menu">
                    <button class="nt-safe-space-contact-menu-item nt-safe-space-btn nt-safe-space-btn-mute">
                        <span class="nt-safe-space-contact-menu-icon">${muteIconSVG}</span>
                        <span class="nt-safe-space-contact-menu-label">Mute</span>
                    </button>
                    <button class="nt-safe-space-contact-menu-item nt-safe-space-btn nt-safe-space-btn-block">
                        <span class="nt-safe-space-contact-menu-icon">${blockIconSVG}</span>
                        <span class="nt-safe-space-contact-menu-label">Block</span>
                    </button>
                 </div>
            </div>`
 
        const handleContactOptionButtonClick = (e) => {
            e.preventDefault()
            const targetElement = e.target.closest(".nt-safe-space-btn"),
                userContact = e.target.closest(".nt-safe-space-contact-item"),
                targetUserID = parseInt(userContact?.dataset.user, 10)
            if (!targetUserID) {
                return
            }
            if (targetElement.classList.contains("nt-safe-space-btn-mute")) {
                muteUser(targetUserID)
            } else if (targetElement.classList.contains("nt-safe-space-btn-unmute")) {
                unmuteUser(targetUserID)
            } else if (targetElement.classList.contains("nt-safe-space-btn-block")) {
                blockUser(targetUserID)
            }
        }
 
        // Chat Message Template
        const chatMessageTemplate = document.createElement("div")
        chatMessageTemplate.classList.add("nt-safe-space-chatroom-message")
        chatMessageTemplate.innerHTML = `
            <div class="nt-safe-space-chatroom-message-heading">
                <img class="icon icon-nt-gold-s" src="/dist/site/images/themes/profiles/gold/nt-gold-icon-xl.png" alt="Nitro Gold">
                <span class="nt-safe-space-chatroom-message-team"></span>
                <span class="nt-safe-space-chatroom-message-name nt-gold-user"></span>
                <span class="nt-safe-space-chatroom-message-friend">${friendIconSVG}</span>
                <div class="nt-safe-space-chatroom-message-time"></div>
            </div>
            <div class="nt-safe-space-chatroom-message-body">
                <span class="nt-safe-space-chatroom-message-text"></span>
                <div class="nt-safe-space-chatroom-mesasge-img"></div>
            </div>`
 
        const chatNameTemplate = chatMessageTemplate.querySelector(".nt-safe-space-chatroom-message-heading").cloneNode(true)
        chatNameTemplate.querySelector(".nt-safe-space-chatroom-message-time").remove()
 
        // Setup Custom Sticker Shortcut Handler
        const handleKeyPress = (t, n) => {
            if (t !== "keydown") {
                return false
            }
            let selectedButton
            const { key } = n
 
            if (key.toLowerCase() === "s") {
                selectedButton = buttonSticker
            } else if (key.toLowerCase() === "c" && isFriendRace) {
                selectedButton = buttonChat
            }
            if (selectedButton) {
                if (selectedButton.classList.contains("selected")) {
                    selectedButton.classList.remove("selected")
                    toggleChatOptions(false)
                    return false
                }
                toggleChatOptions(true)
                isStickers = selectedButton.classList.contains("option-sticker")
                refreshChatOptions()
                root.querySelectorAll(".nt-safe-space-chatroom-reply-toolbar-option").forEach((optionElement) => {
                    optionElement.classList.remove("selected")
                })
                selectedButton.classList.add("selected")
                return false
            }
 
            // Handle Chat Send (if the menu is opened)
            const selected = root.querySelector(".nt-safe-space-chatroom-reply-toolbar-option.selected")
            if (!selected) {
                return false
            }
            if (/^[1-8]$/.test(key) && raceKeyboardObj) {
                const index = parseInt(key - 1, 10)
                if (isStickers) {
                    originalChatObj.sendMessage(userStickers[index].id, userStickers[index].src, "sticker")
                } else {
                    originalChatObj.sendMessage(index, raceObj.props.chatTexts[index], "text")
                }
                flashChatButton(index)
                return false
            }
        }
 
        const addMessage = (type, user, message, imgSrc) => {
            const { userID } = user,
                { tag, tagColor, displayName, username } = user.profile,
                isMe = userID == currentUserID,
                isGold = user.profile.membership === "gold",
                isFriend = friendIDs && friendIDs.includes(userID) ? true : user.isFriend,
                newMessageElement = chatMessageTemplate.cloneNode(true),
                stamp = new Date(),
                newMessageTeamNode = newMessageElement.querySelector(".nt-safe-space-chatroom-message-team"),
                newMessageNameNode = newMessageElement.querySelector(".nt-safe-space-chatroom-message-name"),
                newMessageTextNode = newMessageElement.querySelector(".nt-safe-space-chatroom-message-text"),
                newMessageImageNode = newMessageElement.querySelector(".nt-safe-space-chatroom-mesasge-img")
            newMessageElement.querySelector(".nt-safe-space-chatroom-message-time").textContent = `- ${stamp.toLocaleTimeString("en-US")}`
            newMessageElement.dataset.user = userID
            newMessageNameNode.textContent = displayName || username
            newMessageTextNode.textContent = message
            if (isMe) {
                newMessageElement.classList.add("is-me")
            }
            if (tag) {
                newMessageTeamNode.textContent = `[${tag}]`
                newMessageTeamNode.style.color = `#${tagColor}`
            } else {
                newMessageTeamNode.remove()
            }
            if (!isGold) {
                newMessageNameNode.classList.remove("nt-gold-user")
                newMessageElement.querySelector(".icon-nt-gold-s").remove()
            }
            if (!isFriend) {
                newMessageElement.querySelector(".nt-safe-space-chatroom-message-friend").remove()
            }
            if (type === "system") {
                newMessageTextNode.classList.add("system-message")
            }
            if (imgSrc) {
                newMessageImageNode.style.backgroundImage = `url(${imgSrc})`
            } else {
                newMessageImageNode.remove()
            }
 
            chatMessages.appendChild(newMessageElement)
            chatMessages.scrollTop = chatMessages.scrollHeight
        }
 
        // Return Chat component
        return {
            root: root,
            systemUser,
            addRacer,
            removeRacer,
            getRacer,
            addMessage,
            updateUser,
            assignStickers: (stickers = []) => {
                userStickers = stickers
                refreshChatOptions()
            },
            enableKeyListener: (kbObj, chatObj) => {
                if (!kbObj) {
                    throw new Error("Keyboard React Object is required")
                }
                if (!chatObj) {
                    throw new Error("Chat React Object is required")
                }
                originalChatObj = chatObj
                raceKeyboardObj = kbObj
                raceKeyboardObj.input.initialize({
                    boundElement: raceKeyboardObj.typingInputRef.current,
                    keyHandler: (t, n) => {
                        let continueEvent = true
                        if (!raceKeyboardObj.props.started) {
                            continueEvent = handleKeyPress(t, n)
                        }
                        if (continueEvent) {
                            raceKeyboardObj.handleKeyPress(t, n)
                        }
                    },
                })
            },
            disableChat: () => {
                toggleChat(false)
                if (raceKeyboardObj) {
                    raceKeyboardObj.input.initialize({
                        boundElement: raceKeyboardObj.typingInputRef.current,
                        keyHandler: raceKeyboardObj.handleKeyPress,
                    })
                }
            },
            displaySpeechBubble: (userID, imgSrc) => {
                const speechBubble = document.querySelector(`#${racerContactIDPrefix}${userID} .nt-safe-space-contact-speech-bubble-img`)
                if (speechBubble) {
                    if (userSpeechBubbleTimer[userID]) {
                        clearTimeout(userSpeechBubbleTimer[userID])
                    }
                    speechBubble.style.backgroundImage = `url(${imgSrc})`
                    speechBubble.parentNode.classList.remove("nt-safe-space-hidden")
                    userSpeechBubbleTimer[userID] = setTimeout(() => {
                        speechBubble.parentNode.classList.add("nt-safe-space-hidden")
                        userSpeechBubbleTimer[userID] = null
                    }, 4e3)
                }
            },
            getChatUser: (userID) => {
                return db.users.get(userID)
            },
        }
    })(root, raceObj, db)
 
    /** Displays Information about Race Status and Results. */
    const InfoSection = (() => {
        const root = document.createElement("div")
        root.classList.add("nt-safe-space-info")
        root.innerHTML = `
            <div class="nt-safe-space-info-status">
                <div class="nt-safe-space-info-status-title">Setting up Typing Test</div>
                <div class="nt-safe-space-info-status-subtitle"></div>
                <div class="nt-safe-space-info-status-wampus"><img src="/images/loot/sticker_1630508306.png" alt="Laughing Wampus" /></div>
            </div>
            <div class="nt-safe-space-info-footer">
            </div>`
 
        const status = root.querySelector(".nt-safe-space-info-status"),
            statusTitle = root.querySelector(".nt-safe-space-info-status-title"),
            statusSubTitle = root.querySelector(".nt-safe-space-info-status-subtitle"),
            statusWampus = root.querySelector(".nt-safe-space-info-status-wampus"),
            statusFooter = root.querySelector(".nt-safe-space-info-footer")
 
        statusSubTitle.remove()
        statusWampus.remove()
 
        const updateStatusTitle = (text) => {
            statusTitle.textContent = text
        }
 
        const updateStatusSubTitle = (text) => {
            if (text) {
                statusSubTitle.textContent = text
                if (statusWampus.isConnected) {
                    statusWampus.before(statusSubTitle)
                } else {
                    status.append(statusSubTitle)
                }
            } else {
                statusSubTitle.remove()
            }
        }
 
        const updatePlayer = (node) => {
            statusFooter.append(node)
        }
 
        const toggleWampus = (show) => {
            if (show) {
                status.append(statusWampus)
            } else {
                statusWampus.remove()
            }
        }
 
        const COUNTDOWN_STATES = [["Get Ready!", "It's Typing Test Time! Get ready..."], ["3..."], ["2..."], ["1..."], ["Let's Go!", "Go go go! GLHF!"]]
        let countdownTimer,
            lastCountdown = 0
 
        const updateText = (state, chat) => {
            let [status, systemChatMessage] = COUNTDOWN_STATES[state]
            systemChatMessage = systemChatMessage || status
            chat.addMessage("system", chat.systemUser, systemChatMessage)
            updateStatusTitle(status)
        }
 
        return {
            root,
            updateStatusTitle,
            updateStatusSubTitle,
            updatePlayer,
            toggleWampus,
            startCountdown: (chat) => {
                if (countdownTimer) {
                    logging.warn("Status")("You can only initiate countdown once")
                    return
                }
                lastCountdown = 0
                updateText(lastCountdown, chat)
                countdownTimer = setInterval(() => {
                    if (lastCountdown + 1 < COUNTDOWN_STATES.length - 1) {
                        updateText(++lastCountdown, chat)
                    }
                }, 1e3)
            },
            stopCountdown: (chat) => {
                clearTimeout(countdownTimer)
                lastCountdown = COUNTDOWN_STATES.length - 1
                updateText(lastCountdown, chat)
            },
        }
    })()
 
    ////////////////////////
    //  Backend Handling  //
    ////////////////////////
 
    let disqualifiedUsers = [],
        reloadRaceRequested = false,
        canReloadRace = false,
        isWampusRace = false
 
    const server = raceObj.server,
        currentUserID = raceObj.props.user.userID,
        friendIDs = raceObj.props.friendIDs,
        stickerList = raceObj.stickers,
        chatTextList = raceObj.props.chatTexts
 
    /** Reload next race earlier than usual. */
    const requestNextRaceASAP = (e) => {
        ChatRoom.addMessage("system", ChatRoom.systemUser, "No don't leave me :(")
        if (canReloadRace) {
            InfoSection.updateStatusSubTitle("Starting new race...")
            raceObj.raceAgain(e)
            return
        }
        reloadRaceRequested = true
        InfoSection.updateStatusSubTitle("Loading new race, please wait...")
    }
 
    /** Key Event handler to allow early race reloading. */
    const nextRaceASAPKeyHandler = (e) => {
        if (e.key === "Enter") {
            window.removeEventListener("keypress", nextRaceASAPKeyHandler)
            requestNextRaceASAP(e)
        }
    }
 
    // Setup User's stickers
    let userStickers = raceObj.userStickers
        .filter((s) => s.equipped)
        .sort((a, b) => a.equipped - b.equipped)
        .map((s) => s.lootID)
    if (userStickers.length === 0) {
        userStickers = raceObj.props.lootConfig.sticker.defaults
    }
    ChatRoom.assignStickers(
        userStickers.map((id) => {
            const s = raceObj.props.loot.find((s) => s.lootID === id)
            return {
                id,
                name: s.name,
                src: s.options.src,
            }
        })
    )
 
    // Track Speed Range and Race Mode
    server.on("setup", (e) => {
        if (typeof e.trackLeader === "string" && e.trackLeader !== "") {
            if (e.trackLeader === raceObj.props.user.username) {
                InfoSection.updateStatusTitle("Creating Friendly Typing Test")
            } else {
                InfoSection.updateStatusTitle("Joining Friendly Typing Test")
            }
        }
 
        let subTitle = ""
        if ((typeof e.trackLeader !== "string" || e.trackLeader !== raceObj.props.user.username) && e.scores && e.scores.length === 2) {
            const [from, to] = e.scores
            subTitle = `Speed Range: ${from} WPM - ${to} WPM`
        } else {
            subTitle = `${e.scoringMode.toUpperCase()} mode`
            server.on("settings", (e) => {
                InfoSection.updateStatusSubTitle(`${e.scoringMode.toUpperCase()} mode`)
            })
        }
        if (subTitle) {
            InfoSection.updateStatusSubTitle(subTitle)
        }
    })
 
    // Track Race Status
    server.on("status", (e) => {
        const raceStatus = e.status
        if (raceStatus === "countdown") {
            logging.info("Racing")("Start countdown")
            if (isWampusRace) {
                InfoSection.toggleWampus(false)
            }
            InfoSection.startCountdown(ChatRoom)
        } else if (raceStatus === "racing") {
            logging.info("Racing")("Start racing")
            InfoSection.stopCountdown(ChatRoom)
            ChatRoom.disableChat()
 
            const lastLetter = raceContainer.querySelector(".dash-copy .dash-word:last-of-type .dash-letter:nth-last-of-type(2)")
            if (lastLetter) {
                lastLetterObserver.observe(lastLetter, { attributes: true })
            } else {
                logging.warn("Init")("Unable to setup finish race tracker")
            }
        }
    })
 
    // Track New Racers
    server.on("joined", (user) => {
        if (user.robot) {
            if (user.profile.specialRobot === "wampus") {
                isWampusRace = true
                InfoSection.toggleWampus(true)
            }
            return
        }
        ChatRoom.getChatUser(user.userID).then((data) => {
            if (!data || data.status !== "BLOCK") {
                const chatNode = ChatRoom.addRacer(user, data?.status)
                if (user.userID === currentUserID) {
                    InfoSection.updatePlayer(chatNode)
                }
                ChatRoom.addMessage("system", user, "Has joined the chatroom")
            }
            if (data?.status === "MUTE") {
                ChatRoom.addMessage("system", user, "Has been muted =)")
            }
            if (data?.status === "BLOCK") {
                logging.info("Chat")("This user is blocked", JSON.stringify(user))
            }
            if (data) {
                ChatRoom.updateUser(user.userID, data.status, user).then(() => {
                    logging.info("Chat")(`Sync user details (${data.status})`, JSON.stringify(user))
                })
            }
        })
    })
 
    // Track Players Leaving (Friend Race)
    server.on("left", (e) => {
        if (!e) {
            return
        }
        ChatRoom.getChatUser(e).then((data) => {
            if (data?.status !== "BLOCK") {
                const user = ChatRoom.getRacer(e)
                if (user) {
                    ChatRoom.addMessage("system", user, "Has left the chatroom =(")
                }
            }
        })
        ChatRoom.removeRacer(e)
    })
 
    // Track New Chat Messages
    server.on("chat", (e) => {
        const user = raceObj.state.racers.find((r) => r.userID === e.from)
        if (!user) {
            logging.warn("Chat")("Received message from unknown user", JSON.stringify(e))
            return
        }
        ChatRoom.getChatUser(user.userID).then((data) => {
            let message, imgSrc
            if (e.chatType === "sticker" && stickerList) {
                const sticker = stickerList.find((s) => s.lootID === e.chatID)
                if (sticker) {
                    message = sticker.name
                    imgSrc = sticker.options.src
                }
            } else if (e.chatType === "text" && chatTextList) {
                message = chatTextList[e.chatID]
                imgSrc = `/dist/site/images/chat/canned/chat_${e.chatID}.png`
            } else {
                message = "???"
            }
            if (!data || !["MUTE", "BLOCK"].includes(data.status)) {
                ChatRoom.addMessage("msg", user, message, imgSrc)
                ChatRoom.displaySpeechBubble(user.userID, imgSrc)
            } else {
                logging.info("Chat")(`${data.status} message received`, JSON.stringify({ ...e, message, imgSrc }))
            }
        })
    })
 
    // Track Racing Updates for disqualify and completion
    server.on("update", (e) => {
        e?.racers?.forEach((user) => {
            if (!user.robot && user.disqualified && !disqualifiedUsers.includes(user.userID)) {
                disqualifiedUsers = disqualifiedUsers.concat(user.userID)
                ChatRoom.getChatUser(user.userID).then((data) => {
                    if (data?.status !== "BLOCK") {
                        ChatRoom.addMessage("system", user, "Has left the chatroom =(")
                    }
                })
            }
            if (user.userID === currentUserID && user.progress.completeStamp > 0 && user.profile && !canReloadRace && !raceContainer.querySelector(".race-results")) {
                if (reloadRaceRequested) {
                    InfoSection.updateStatusSubTitle("Starting new race...")
                    raceObj.raceAgain(e)
                } else {
                    canReloadRace = true
                }
            }
        })
    })
 
    /** Rank suffixes for Race Result. */
    const RANK_SUFFIX = ["st", "nd", "rd"]
 
    /** Mutation obverser to track whether results screen showed up. */
    const resultObserver = new MutationObserver(([mutation], observer) => {
        for (const newNode of mutation.addedNodes) {
            if (newNode.classList?.contains("race-results")) {
                observer.disconnect()
                window.removeEventListener("keypress", nextRaceASAPKeyHandler)
 
                const currentUserResult = raceObj.state.racers.find((r) => r.userID === currentUserID)
                if (!currentUserResult || !currentUserResult.progress || typeof currentUserResult.place === "undefined") {
                    logging.warn("Finish")("Unable to find race results")
                    return
                }
 
                const resultMain = raceContainer.querySelector(".raceResults"),
                    resultContainer = resultMain.parentNode,
                    obj = resultContainer ? findReact(resultContainer) : null
                if (!resultContainer || !obj) {
                    logging.warn("Finish")("Unable to hide result screen by default")
                    return
                }
                resultMain.style.marginLeft = "-10000px"
                resultContainer.classList.add("is-minimized", "has-minimized")
                obj.state.isHidden = true
                obj.state.hasMinimized = true
 
                setTimeout(() => {
                    resultMain.style.marginLeft = ""
                }, 500)
 
                const { typed, skipped, startStamp, completeStamp, errors } = currentUserResult.progress,
                    wpm = Math.round((typed - skipped) / 5 / ((completeStamp - startStamp) / 6e4)),
                    time = ((completeStamp - startStamp) / 1e3).toFixed(2),
                    acc = ((1 - errors / (typed - skipped)) * 100).toFixed(2),
                    points = Math.round((100 + wpm / 2) * (1 - errors / (typed - skipped))),
                    place = currentUserResult.place,
                    rankSuffix = place >= 1 && place <= 3 ? RANK_SUFFIX[place - 1] : "th"
                InfoSection.updateStatusTitle("Race Results")
                InfoSection.updateStatusSubTitle(`${place}${rankSuffix} | ${acc}% Acc | ${wpm} WPM | ${points} points | ${time} secs`)
 
                logging.info("Finish")("Display Alternative Result Screen")
                break
            }
        }
    })
 
    /** Mutation observer to track whether last letter was typed (just finished race). */
    const lastLetterObserver = new MutationObserver(([mutation], observer) => {
        if (mutation.target.classList.contains("is-correct")) {
            observer.disconnect()
            window.addEventListener("keypress", nextRaceASAPKeyHandler)
            InfoSection.updateStatusTitle("Finished")
            if (isWampusRace) {
                InfoSection.toggleWampus(true)
            }
            ChatRoom.addMessage("system", ChatRoom.systemUser, "Done! Time to review your result :)")
            resultObserver.observe(raceContainer, { childList: true })
        }
    })
 
    /////////////
    //  Final  //
    /////////////
 
    // Hide chat (Nitro Type will break if the DOM element is removed)
    const registerChatNode = (node, inputNode) => {
        if (!node?.classList?.contains("raceChat")) {
            if (node !== null) {
                logging.warn("Init")("Invalid node element for registering chat.")
            }
            return
        }
 
        const raceKeyboardObj = findReact(inputNode),
            originalChatObj = findReact(node)
        node.style.display = "none"
        if (raceKeyboardObj && originalChatObj) {
            ChatRoom.enableKeyListener(raceKeyboardObj, originalChatObj)
        } else {
            logging.warn("Init")("Unable to overwrite chat system")
        }
    }
    const typingInputObserver = new MutationObserver((mutations, observer) => {
        for (const m of mutations) {
            for (const node of m.addedNodes) {
                if (node.classList?.contains("dash-copy-input")) {
                    observer.disconnect()
                    const raceChatNode = raceContainer.querySelector(".raceChat")
                    if (raceChatNode) {
                        registerChatNode(raceChatNode, node)
                    } else {
                        logging.warn("Init")("Unable to overwrite chat system")
                    }
                    break
                }
            }
        }
    })
 
    const raceChatNode = raceContainer.querySelector(".raceChat"),
        typingInputNode = raceContainer.querySelector(".dash-copy-input")
    if (raceChatNode && typingInputNode) {
        registerChatNode(raceChatNode, typingInputNode)
    } else {
        typingInputObserver.observe(raceContainer.querySelector(".dash-center"), { childList: true })
    }
 
    // Setup Race Track
    root.append(InfoSection.root, ChatRoom.root)
 
    // Replace Race Track
    canvasTrack.replaceWith(root)
 
    logging.info("Init")("Race Track has been updated")
}