Long-press the Faction chat button to pin chat to that window. Orange glow = pinned.
// ==UserScript==
// @name ChatLock
// @namespace https://torn.com
// @version 3.1.0
// @description Long-press the Faction chat button to pin chat to that window. Orange glow = pinned.
// @author Prodigal
// @match https://www.torn.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const LS_KEY = 'ChatLock_owner';
const SS_KEY = 'ChatLock_tabId';
const PARAM = 'chat';
const HOLD_MS = 600;
// Stable structural selector — targets the panel container, not hashed React classes
const HIDE_CSS = '#chatRoot > div > div:first-child { display: none !important; }';
const PINNED_CSS = `
[id^="channel_panel_button:faction-"].cl-pinned {
background: linear-gradient(#7a3a00, #c47d10) !important;
box-shadow: 0 0 10px rgba(245,166,35,0.55), inset 0 1px 0 rgba(255,255,255,0.15) !important;
}
[id^="channel_panel_button:faction-"].cl-pinned:hover {
background: linear-gradient(#9a4a00, #f5a623) !important;
}
[id^="channel_panel_button:faction-"].cl-pinned svg {
filter: brightness(10) !important;
}
[id^="channel_panel_button:faction-"].cl-charging {
outline: 2px solid rgba(245,166,35,0.85) !important;
outline-offset: 2px !important;
}
`;
// ── Tab identity ────────────────────────────────────────────────────────────
// sessionStorage is per-tab, so this ID dies when the tab closes
let myTabId = sessionStorage.getItem(SS_KEY);
if (!myTabId) {
myTabId = `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
sessionStorage.setItem(SS_KEY, myTabId);
}
// Reload with ?chat=1 writes this tab as owner before React renders
if (new URLSearchParams(window.location.search).get(PARAM) === '1') {
localStorage.setItem(LS_KEY, myTabId);
}
// ── Core helpers ────────────────────────────────────────────────────────────
const isChatOwner = () => localStorage.getItem(LS_KEY) === myTabId;
function injectStyle(id, css) {
if (document.getElementById(id)) return;
const el = document.createElement('style');
el.id = id;
el.textContent = css;
(document.head || document.documentElement).appendChild(el);
}
// Suppress or restore chat panels based on ownership
function applyState() {
if (isChatOwner()) {
document.getElementById('cl-hide')?.remove();
} else {
injectStyle('cl-hide', HIDE_CSS);
}
}
// Inject immediately — before React renders, so panels never flash open
applyState();
// ── Pin / release ───────────────────────────────────────────────────────────
function navigate(addParam) {
const url = new URL(window.location.href);
addParam ? url.searchParams.set(PARAM, '1') : url.searchParams.delete(PARAM);
window.location.replace(url);
}
function claimPin() { localStorage.setItem(LS_KEY, myTabId); navigate(true); }
function releasePin() { localStorage.removeItem(LS_KEY); navigate(false); }
// ── Button highlight ────────────────────────────────────────────────────────
function applyHighlight(btn) {
const owned = isChatOwner();
btn.classList.toggle('cl-pinned', owned);
btn.title = owned ? 'Faction (pinned — hold to release)' : 'Faction';
}
// ── Long-press ──────────────────────────────────────────────────────────────
function attachLongPress(btn) {
if (btn.dataset.clAttached) return;
btn.dataset.clAttached = '1';
let timer = null;
let fired = false;
btn.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
fired = false;
btn.classList.add('cl-charging');
timer = setTimeout(() => {
fired = true;
btn.classList.remove('cl-charging');
isChatOwner() ? releasePin() : claimPin();
}, HOLD_MS);
});
const cancel = () => { clearTimeout(timer); btn.classList.remove('cl-charging'); };
btn.addEventListener('mouseup', cancel);
btn.addEventListener('mouseleave', cancel);
// Intercept click in capture phase to suppress Torn's handler if we just fired
btn.addEventListener('click', (e) => { if (fired) { e.stopImmediatePropagation(); fired = false; } }, true);
}
// ── Init ────────────────────────────────────────────────────────────────────
function init(btn) {
injectStyle('cl-pinned-styles', PINNED_CSS);
applyHighlight(btn);
attachLongPress(btn);
}
function waitForBtn() {
const btn = document.querySelector('[id^="channel_panel_button:faction-"]');
if (btn) { init(btn); return; }
const obs = new MutationObserver(() => {
const btn = document.querySelector('[id^="channel_panel_button:faction-"]');
if (btn) { obs.disconnect(); init(btn); }
});
obs.observe(document.body, { childList: true, subtree: true });
}
// Cross-tab sync: another window claimed or released the pin
window.addEventListener('storage', (e) => {
if (e.key !== LS_KEY) return;
applyState();
const btn = document.querySelector('[id^="channel_panel_button:faction-"]');
if (btn) applyHighlight(btn);
});
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', waitForBtn)
: waitForBtn();
})();