Trade Chat Timer on Button

Show a timer that shows the time left to post next message, synchronized across tabs.

  1. // ==UserScript==
  2. // @name Trade Chat Timer on Button
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.3
  5. // @description Show a timer that shows the time left to post next message, synchronized across tabs.
  6. // @match https://www.torn.com/*
  7. // ==/UserScript==
  8.  
  9. const STORAGE_KEY = "localStorage__Trade_Chat_Timer__Do_Not_Edit";
  10. const timerChannel = new BroadcastChannel('tradeChatTimer');
  11.  
  12. function waitFor(selector, parent = document) {
  13. return new Promise(resolve => {
  14. const observer = new MutationObserver(mutations => {
  15. mutations.forEach(() => {
  16. const el = parent.querySelector(selector);
  17. if (el) {
  18. observer.disconnect();
  19. resolve(el);
  20. }
  21. });
  22. });
  23. observer.observe(parent, { childList: true, subtree: true });
  24. });
  25. }
  26.  
  27. (async () => {
  28. const addStyle = () => {
  29. if (!document.head.querySelector("#trade-chat-timer-style")) {
  30. const style = document.createElement('style');
  31. style.id = "trade-chat-timer-style";
  32. style.textContent = `
  33. #chatRoot [class*="minimized-menu-item__"][title="Trade"] {
  34. position: relative;
  35. }
  36. #chatRoot [class*="minimized-menu-item__"][title="Trade"] .timer-svg {
  37. position: absolute;
  38. top: 0;
  39. left: 0;
  40. width: 100%;
  41. height: 100%;
  42. pointer-events: none;
  43. }
  44. `;
  45. document.head.appendChild(style);
  46. }
  47. };
  48.  
  49. addStyle();
  50.  
  51. const tradeChatButton = await waitFor("#chatRoot [class*='minimized-menu-item__'][title='Trade']");
  52. let tradeChat = tradeChatButton?.className.includes("minimized-menu-item--open__") ? await getTradeChat() : null;
  53.  
  54. let timerSvg = tradeChatButton?.querySelector('.timer-svg');
  55. if (tradeChatButton && !timerSvg) {
  56. timerSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  57. timerSvg.setAttribute("viewBox", "0 0 100 100");
  58. timerSvg.classList.add("timer-svg");
  59. timerSvg.innerHTML = '<rect x="5" y="5" width="90" height="90" stroke-width="10" fill="none" />';
  60. tradeChatButton.appendChild(timerSvg);
  61. }
  62.  
  63. const timerRect = timerSvg?.querySelector('rect');
  64.  
  65. let nextAllowedTime = new Date(localStorage.getItem(STORAGE_KEY) || Date.now());
  66.  
  67. const updateTimerVisual = (timeLeft) => {
  68. const roundedTimeLeft = Math.round(timeLeft / 1000) * 1000; // Round to nearest second
  69. if (timerRect) {
  70. if (roundedTimeLeft > 0) {
  71. timerRect.setAttribute('stroke', 'red');
  72. timerRect.setAttribute('stroke-dasharray', '360');
  73. timerRect.setAttribute('stroke-dashoffset', 360 * (1 - roundedTimeLeft / 60000));
  74. } else {
  75. timerRect.setAttribute('stroke', 'green');
  76. timerRect.setAttribute('stroke-dasharray', 'none');
  77. timerRect.setAttribute('stroke-dashoffset', '0');
  78. }
  79. }
  80. };
  81.  
  82. let timerInterval;
  83. const setTimer = () => {
  84. const now = new Date();
  85. const timeUntil = Math.max(nextAllowedTime - now, 0);
  86. updateTimerVisual(timeUntil);
  87.  
  88. timerChannel.postMessage({ timeUntil });
  89.  
  90. if (timeUntil === 0) {
  91. clearInterval(timerInterval);
  92. } else {
  93. requestAnimationFrame(setTimer);
  94. }
  95. };
  96.  
  97. const resetTimer = () => {
  98. nextAllowedTime = new Date(Date.now() + 60000);
  99. localStorage.setItem(STORAGE_KEY, nextAllowedTime.toISOString());
  100. clearInterval(timerInterval);
  101. timerInterval = requestAnimationFrame(setTimer);
  102.  
  103. timerChannel.postMessage({ reset: true, nextAllowedTime: nextAllowedTime.toISOString() });
  104. };
  105.  
  106. timerChannel.onmessage = (event) => {
  107. if (event.data.timeUntil !== undefined) {
  108. updateTimerVisual(event.data.timeUntil);
  109. } else if (event.data.reset) {
  110. nextAllowedTime = new Date(event.data.nextAllowedTime);
  111. clearInterval(timerInterval);
  112. timerInterval = requestAnimationFrame(setTimer);
  113. }
  114. };
  115.  
  116. async function checkForBlockMessage(chatBody) {
  117. const lastMessage = chatBody?.lastElementChild;
  118. return lastMessage && lastMessage.classList.contains("chat-box-body__block-message-wrapper___JjbKr") && lastMessage.textContent.includes("Trade chat allows one message per 60 seconds");
  119. }
  120.  
  121. async function handleNewMessage(chat) {
  122. const chatBody = chat.querySelector("[class*='chat-box-body___']");
  123. return new Promise(resolve => {
  124. const observer = new MutationObserver(async (mutations) => {
  125. const mutation = mutations.find(mutation => mutation.addedNodes.length);
  126. if (!mutation) return;
  127.  
  128. observer.disconnect();
  129.  
  130. if (await checkForBlockMessage(chatBody)) {
  131. resolve(false);
  132. } else {
  133. resetTimer();
  134. resolve(true);
  135. }
  136. });
  137.  
  138. observer.observe(chatBody, { childList: true });
  139. });
  140. }
  141.  
  142. function attachChatListeners(chat) {
  143. const textarea = chat.querySelector("textarea");
  144. if (textarea) {
  145. textarea.addEventListener("keyup", async e => {
  146. if (e.key === "Enter") {
  147. await handleNewMessage(chat);
  148. }
  149. });
  150. }
  151.  
  152. const sendButton = chat.querySelector("button.chat-box-footer__send-icon-wrapper___fGx9E");
  153. if (sendButton) {
  154. sendButton.addEventListener("click", async () => {
  155. await handleNewMessage(chat);
  156. });
  157. }
  158. }
  159.  
  160. if (tradeChat) {
  161. attachChatListeners(tradeChat);
  162. }
  163.  
  164. tradeChatButton?.addEventListener("click", async () => {
  165. if (!tradeChatButton.className.includes("minimized-menu-item--open__")) {
  166. tradeChat = await getTradeChat();
  167. }
  168. if (tradeChat) {
  169. attachChatListeners(tradeChat);
  170. }
  171. });
  172.  
  173. setTimer(); // Initial timer setup
  174. timerInterval = requestAnimationFrame(setTimer);
  175.  
  176. async function getTradeChat() {
  177. await waitFor("#chatRoot [class*='chat-box-header__']");
  178. return [...document.querySelectorAll("#chatRoot [class*='chat-box-header__']")].find(x => x.textContent === "Trade")?.closest("[class*='chat-box__']");
  179. }
  180. })();