Trade Chat Timer on Button

Show a timer that shows the time left to post next message.

目前為 2024-07-11 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Trade Chat Timer on Button
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Show a timer that shows the time left to post next message.
// @match        https://www.torn.com/*
// ==/UserScript==

const STORAGE_KEY = "localStorage__Trade_Chat_Timer__Do_Not_Edit";

async function waitFor(selector, parent = document) {
    return new Promise(resolve => {
        const checkExist = () => {
            const el = parent.querySelector(selector);
            if (el) {
                resolve(el);
            } else {
                requestAnimationFrame(checkExist);
            }
        };
        checkExist();
    });
}

function addStyle() {
    if (!document.head.querySelector("#trade-chat-timer-style")) {
        const style = document.createElement('style');
        style.id = "trade-chat-timer-style";
        style.textContent = `
            #chatRoot [class*="minimized-menu-item__"][title="Trade"] {
                position: relative;
            }
            #chatRoot [class*="minimized-menu-item__"][title="Trade"] .timer-svg {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                pointer-events: none;
            }
        `;
        document.head.appendChild(style);
    }
}

function createTimerSvg() {
    const timerSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    timerSvg.setAttribute("viewBox", "0 0 100 100");
    timerSvg.classList.add("timer-svg");
    timerSvg.innerHTML = '<rect x="5" y="5" width="90" height="90" stroke-width="10" fill="none" />';
    return timerSvg;
}

function updateTimerVisual(timerRect, timeLeft) {
    if (timeLeft > 0) {
        timerRect.setAttribute('stroke', 'red');
        timerRect.setAttribute('stroke-dasharray', '360');
        timerRect.setAttribute('stroke-dashoffset', 360 * (1 - timeLeft / 60000));
    } else {
        timerRect.setAttribute('stroke', 'green');
        timerRect.setAttribute('stroke-dasharray', 'none');
        timerRect.setAttribute('stroke-dashoffset', '0');
    }
}

function setTimer(timerRect, nextAllowedTime) {
    const now = new Date();
    const timeUntil = Math.max(nextAllowedTime - now, 0);
    updateTimerVisual(timerRect, timeUntil);
    return timeUntil;
}

function resetTimer(timerRect, nextAllowedTime) {
    nextAllowedTime.setTime(Date.now() + 60000);
    localStorage.setItem(STORAGE_KEY, nextAllowedTime.toISOString());
    setTimer(timerRect, nextAllowedTime);
}

async function checkForBlockMessage(chatBody) {
    const lastMessage = chatBody.lastElementChild;
    return lastMessage && lastMessage.classList.contains("chat-box-body__block-message-wrapper___JjbKr") && lastMessage.textContent.includes("Trade chat allows one message per 60 seconds");
}

async function handleNewMessage(chat, timerRect, nextAllowedTime) {
    const chatBody = chat.querySelector("[class*='chat-box-body___']");
    return new Promise(resolve => {
        const observer = new MutationObserver(async (mutations) => {
            const mutation = mutations.find(mutation => mutation.addedNodes.length);
            if (!mutation) return;

            observer.disconnect();

            if (await checkForBlockMessage(chatBody)) {
                resolve(false);
            } else {
                resetTimer(timerRect, nextAllowedTime);
                resolve(true);
            }
        });
        observer.observe(chatBody, { childList: true });
    });
}

function attachKeyUpListener(chat, timerRect, nextAllowedTime) {
    const textarea = chat.querySelector("textarea");
    if (textarea) {
        textarea.addEventListener("keyup", async e => {
            if (e.key === "Enter") {
                await handleNewMessage(chat, timerRect, nextAllowedTime);
            }
        });
    }
}

async function getTradeChat() {
    await waitFor("#chatRoot [class*='chat-box-header__']");
    return [...document.querySelectorAll("#chatRoot [class*='chat-box-header__']")].find(x => x.textContent === "Trade")?.closest("[class*='chat-box__']");
}

(async () => {
    addStyle();

    const tradeChatButton = await waitFor("#chatRoot [class*='minimized-menu-item__'][title='Trade']");
    let tradeChat = tradeChatButton.className.includes("minimized-menu-item--open__") ? await getTradeChat() : null;

    const timerSvg = createTimerSvg();
    tradeChatButton.appendChild(timerSvg);

    const timerRect = timerSvg.querySelector('rect');
    let nextAllowedTime = new Date(localStorage.getItem(STORAGE_KEY) || Date.now());

    setTimer(timerRect, nextAllowedTime);
    let timerInterval = setInterval(() => setTimer(timerRect, nextAllowedTime), 1000);

    if (tradeChat) {
        attachKeyUpListener(tradeChat, timerRect, nextAllowedTime);
    }

    tradeChatButton.addEventListener("click", async () => {
        if (!tradeChatButton.className.includes("minimized-menu-item--open__")) {
            tradeChat = await getTradeChat();
        }
        if (tradeChat) {
            attachKeyUpListener(tradeChat, timerRect, nextAllowedTime);
        }
    });

    document.addEventListener("click", async e => {
        const specificButton = document.querySelector("button.chat-box-footer__send-icon-wrapper___fGx9E");
        if (specificButton && specificButton.contains(e.target)) {
            await handleNewMessage(tradeChat, timerRect, nextAllowedTime);
        }
    });
})();