// ==UserScript==
// @name Trade Chat Timer on Button for Chat 3.0
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Show a timer that shows the time left to post next message
// @match https://www.torn.com/*
// ==/UserScript==
(() => {
const STORAGE_KEY = 'tornTradeTimerEnd';
const TIMER_DURATION = 62000;
let animFrameId = null;
let svgEl = null, rectEl = null, pathLength = 0;
let observer = null;
const createSVGElement = (type, attributes = {}) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', type);
Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value));
return el;
};
const setupOverlay = (btn) => {
const { width, height } = btn.getBoundingClientRect();
if (!svgEl) {
svgEl = createSVGElement('svg', {
id: 'trade-timer-overlay',
style: 'position:absolute;top:0;left:0;z-index:1000;pointer-events:none'
});
rectEl = createSVGElement('rect', {
x: '1.5',
y: '1.5',
fill: 'none',
'stroke-width': '3'
});
svgEl.appendChild(rectEl);
btn.appendChild(svgEl);
}
Object.entries({ width, height }).forEach(([key, value]) => svgEl.setAttribute(key, value));
Object.entries({ width: width - 3, height: height - 3 }).forEach(([key, value]) => rectEl.setAttribute(key, value));
pathLength = 2 * (width + height - 6);
rectEl.setAttribute('stroke-dasharray', pathLength);
};
const updateTimerVisual = (remainingMs) => {
if (!rectEl) return;
const isComplete = remainingMs <= 0;
rectEl.setAttribute('stroke', isComplete ? 'green' : 'red');
rectEl.setAttribute('stroke-dasharray', isComplete ? 'none' : pathLength);
if (!isComplete) {
const offset = pathLength * (1 - remainingMs / TIMER_DURATION);
rectEl.setAttribute('stroke-dashoffset', offset);
}
};
const cleanupTimer = (btn) => {
if (animFrameId) {
cancelAnimationFrame(animFrameId);
animFrameId = null;
}
if (svgEl) {
svgEl.remove();
svgEl = null;
rectEl = null;
}
if (btn) btn.style.border = '3px solid green';
localStorage.removeItem(STORAGE_KEY);
};
const runTimer = (btn) => {
if (!btn) return;
if (animFrameId) cancelAnimationFrame(animFrameId);
btn.style.border = 'none';
// Force reinitialize SVG elements
if (svgEl) {
svgEl.remove();
svgEl = null;
rectEl = null;
}
setupOverlay(btn);
const animate = () => {
const endTime = parseInt(localStorage.getItem(STORAGE_KEY));
const remaining = endTime - Date.now();
if (remaining <= 0) {
cleanupTimer(btn);
} else {
updateTimerVisual(remaining);
animFrameId = requestAnimationFrame(animate);
}
};
const endTime = parseInt(localStorage.getItem(STORAGE_KEY));
updateTimerVisual(endTime - Date.now());
animFrameId = requestAnimationFrame(animate);
};
const startTimer = (btn) => {
if (!btn) return;
localStorage.setItem(STORAGE_KEY, Date.now() + TIMER_DURATION);
runTimer(btn);
};
const handleSend = (ta, btn) => {
if (!ta || !btn) return;
const checkAndStart = () => !ta.value.trim() && startTimer(btn);
checkAndStart() || setTimeout(checkAndStart, 200);
};
const initializeTradeButton = (tradeBtn) => {
if (!tradeBtn) return;
if (getComputedStyle(tradeBtn).position === 'static') {
tradeBtn.style.position = 'relative';
}
const stored = parseInt(localStorage.getItem(STORAGE_KEY));
stored && stored > Date.now() ? runTimer(tradeBtn) : (tradeBtn.style.border = '3px solid green');
};
const setupChat = () => {
const tradeBtn = document.getElementById('channel_panel_button:public_trade');
const ta = document.querySelector('textarea.textarea___V8HsV');
const send = document.querySelector('button.iconWrapper___tyRRU');
if (tradeBtn) initializeTradeButton(tradeBtn);
if (tradeBtn && ta && send) {
const handlers = {
keydown: (e) => e.key === 'Enter' && (e.preventDefault(), handleSend(ta, tradeBtn)),
input: () => !ta.value.trim() && handleSend(ta, tradeBtn),
click: () => handleSend(ta, tradeBtn)
};
Object.entries(handlers).forEach(([event, handler]) => {
const prop = `_timer${event}Handler`;
if (ta[prop]) ta.removeEventListener(event, ta[prop]);
if (send[prop]) send.removeEventListener(event, send[prop]);
const element = event === 'click' ? send : ta;
element[prop] = handler;
element.addEventListener(event, handler);
});
}
};
const setupChatObserver = () => {
if (observer) observer.disconnect();
const chatRoot = document.getElementById('chatRoot');
if (!chatRoot) {
const docObserver = new MutationObserver(() => {
const chatRoot = document.getElementById('chatRoot');
if (chatRoot) {
docObserver.disconnect();
setupChatObserver();
}
});
docObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['id', 'class', 'style']
});
return;
}
observer = new MutationObserver((mutations) => {
const hasRelevantChanges = mutations.some(mutation =>
mutation.type === 'childList' ||
(mutation.type === 'attributes' && ['class', 'style'].includes(mutation.attributeName))
);
if (hasRelevantChanges) setTimeout(setupChat, 100);
});
setupChat();
observer.observe(chatRoot, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style']
});
};
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const tradeBtn = document.getElementById('channel_panel_button:public_trade');
if (tradeBtn) {
const stored = parseInt(localStorage.getItem(STORAGE_KEY));
if (stored && stored > Date.now()) {
runTimer(tradeBtn);
} else {
initializeTradeButton(tradeBtn);
}
setupChat();
}
}
});
window.addEventListener('storage', (e) => {
if (e.key === STORAGE_KEY) {
const tradeBtn = document.getElementById('channel_panel_button:public_trade');
if (!tradeBtn) return;
const val = parseInt(e.newValue);
val && val > Date.now() ? runTimer(tradeBtn) : cleanupTimer(tradeBtn);
}
});
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', setupChatObserver)
: setupChatObserver();
})();