Detects native or site fullscreen and hides the chat once immediately, while still allowing the user to show it again.
// ==UserScript==
// @name Stripchat Native Fullscreen - Smart Exit
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Detects native or site fullscreen and hides the chat once immediately, while still allowing the user to show it again.
// @author Thiago
// @match https://stripchat.com/*
// @match https://*.stripchat.com/*
// @grant none
// ==/UserScript==
// Changelog:
// v1.8 - Hides the chat once immediately after native or site fullscreen starts.
// Supports auto-hidden player controls and lets the user show the chat again.
// v1.7 - Native fullscreen with manual-exit detection and the "T" shortcut.
(function () {
'use strict';
const CHAT_BUTTON_RETRY_MS = 200;
const CHAT_BUTTON_MAX_WAIT_MS = 10000;
const FULLSCREEN_STATE_POLL_MS = 250;
const STATE = {
userExited: false,
autoTried: false,
fullscreenActive: false,
chatClickDone: false,
chatHideTimer: null,
chatSearchTimer: null
};
const isVisible = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return rect.width > 0 &&
rect.height > 0 &&
style.display !== 'none' &&
style.visibility !== 'hidden';
};
const getFSButton = () => {
return document.querySelector('button .icon-fullscreen-on')?.closest('button') ||
document.querySelector('button .icon-fullscreen-off')?.closest('button');
};
const isFullscreenActive = () => {
const siteFullscreenButton = document.querySelector('button .icon-fullscreen-off')?.closest('button');
return Boolean(
document.fullscreenElement ||
(siteFullscreenButton && isVisible(siteFullscreenButton))
);
};
const toggleNativeFS = (force = false) => {
const fsButton = getFSButton();
const isCurrentlyFS = isFullscreenActive();
if (fsButton && (force || (!isCurrentlyFS && !STATE.userExited))) {
fsButton.click();
}
};
const wakeFullscreenControls = () => {
const fullscreenRoot = document.fullscreenElement ||
document.querySelector('.player-controls-fullscreen__main') ||
document.documentElement;
const rect = fullscreenRoot.getBoundingClientRect();
const clientX = Math.round(rect.left + rect.width / 2);
const clientY = Math.round(rect.top + Math.min(rect.height * 0.25, 180));
const targets = [
fullscreenRoot.querySelector('video'),
fullscreenRoot,
document.body,
document,
window
].filter(Boolean);
const mouseOptions = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX,
clientY,
screenX: clientX,
screenY: clientY,
movementX: 1,
movementY: 1
};
for (const target of targets) {
try {
if (typeof PointerEvent === 'function') {
target.dispatchEvent(new PointerEvent('pointermove', {
...mouseOptions,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true
}));
}
target.dispatchEvent(new MouseEvent('mousemove', mouseOptions));
target.dispatchEvent(new MouseEvent('mouseover', mouseOptions));
target.dispatchEvent(new MouseEvent('mouseenter', {
...mouseOptions,
bubbles: false
}));
} catch (_) {}
}
console.log('[SC-FS] Fullscreen controls wake-up requested.');
};
const describeControl = (element) => {
const icon = element.querySelector('[class], svg, i');
return [
element.getAttribute('title'),
element.getAttribute('aria-label'),
element.getAttribute('data-testid'),
element.getAttribute('data-test'),
element.className,
icon?.getAttribute('class'),
element.textContent
].filter(Boolean).join(' ').toLowerCase();
};
const getChatToggleButton = () => {
const fullscreenRoot = document.fullscreenElement || document;
// Known specific selectors take priority.
const specificSelectors = [
'.player-chat-button',
'.fullscreen-chat-toggle',
'.player-chat-toggle',
'.chat-toggle',
'.toggle-chat',
'[class*="chat-toggle" i]',
'[class*="chat_button" i]',
'[data-testid*="chat-toggle" i]',
'button:has(.icon-chat-off)',
'button:has(.icon-chat-on)',
'button:has([class*="chat-disabled" i])',
'button:has([class*="chat-enabled" i])'
];
for (const selector of specificSelectors) {
const match = Array.from(fullscreenRoot.querySelectorAll(selector))
.map((element) => element.closest('button, [role="button"]') || element)
.find(isVisible);
if (match) return match;
}
// Fallback that does not depend on a fixed Stripchat class name.
const candidates = Array.from(
fullscreenRoot.querySelectorAll('button, [role="button"]')
).filter(isVisible);
return candidates.find((element) => {
const description = describeControl(element);
const controlsChatOrInterface = /chat|interface|overlay/.test(description);
const hidesIt = /hide|disable|close|off|ocultar|desabilitar|desativar|fechar/.test(description);
const unrelated = /resolution|quality|qualidade|volume|sound|mute|tip|fullscreen-(on|off)/.test(description);
return controlsChatOrInterface && hidesIt && !unrelated;
}) || null;
};
const chatIsAlreadyHidden = (button) => {
const actionText = [
button.getAttribute('title'),
button.getAttribute('aria-label'),
button.textContent
].filter(Boolean).join(' ').toLowerCase();
// The label describes what clicking will do. "Hide Chat" therefore
// means the chat is visible now and the button must be clicked.
if (/hide|disable|close|ocultar|desabilitar|desativar|fechar/.test(actionText)) return false;
if (/show|enable|open|mostrar|habilitar|ativar/.test(actionText)) return true;
const iconState = [
button.className,
button.querySelector('[class], svg, i')?.getAttribute('class')
].filter(Boolean).join(' ').toLowerCase();
// Stripchat uses icon-chat-enabled while the chat is visible and
// icon-chat-disabled after it has been hidden.
if (/chat-disabled|chat-off|chat-hidden/.test(iconState)) return true;
if (/chat-enabled|chat-on|chat-active/.test(iconState)) return false;
// Fullscreen opens with chat visible by default.
return false;
};
const clearChatTimers = () => {
if (STATE.chatHideTimer) {
clearTimeout(STATE.chatHideTimer);
STATE.chatHideTimer = null;
}
if (STATE.chatSearchTimer) {
clearInterval(STATE.chatSearchTimer);
STATE.chatSearchTimer = null;
}
};
const clickChatToggleOnce = () => {
if (STATE.chatClickDone || !isFullscreenActive()) return true;
const button = getChatToggleButton();
if (!button) return false;
if (chatIsAlreadyHidden(button)) {
STATE.chatClickDone = true;
console.log('[SC-FS] The chat was already hidden; no click was needed.');
return true;
}
// Set the flag before clicking to prevent repetition from synchronous events.
STATE.chatClickDone = true;
console.log('[SC-FS] Hiding the chat/interface with a single click.', button);
button.click();
return true;
};
const findAndClickChatToggle = () => {
wakeFullscreenControls();
return clickChatToggleOnce();
};
const hideChatOnFullscreen = () => {
clearChatTimers();
if (!isFullscreenActive() || STATE.chatClickDone) return;
// The controls are normally still visible at this point, so try now.
if (findAndClickChatToggle()) return;
// If Stripchat has not mounted the button yet, retry lookup without
// performing any extra click after the first successful one.
const searchStartedAt = Date.now();
STATE.chatSearchTimer = setInterval(() => {
const timedOut = Date.now() - searchStartedAt >= CHAT_BUTTON_MAX_WAIT_MS;
const clickedOrAlreadyHidden = findAndClickChatToggle();
if (clickedOrAlreadyHidden || timedOut || !isFullscreenActive()) {
clearInterval(STATE.chatSearchTimer);
STATE.chatSearchTimer = null;
}
}, CHAT_BUTTON_RETRY_MS);
};
const syncFullscreenState = () => {
const fullscreenActive = isFullscreenActive();
if (fullscreenActive === STATE.fullscreenActive) return;
STATE.fullscreenActive = fullscreenActive;
if (fullscreenActive) {
STATE.chatClickDone = false;
console.log('[SC-FS] Fullscreen active; hiding the chat once now.');
hideChatOnFullscreen();
} else {
STATE.userExited = true;
STATE.chatClickDone = false;
clearChatTimers();
console.log('[SC-FS] Manual fullscreen exit detected and respected.');
}
};
document.addEventListener('fullscreenchange', () => {
// Stripchat may update its own fullscreen icon immediately after this event.
setTimeout(syncFullscreenState, 0);
});
// Use the user's first interaction to request fullscreen.
document.addEventListener('click', function once() {
if (!STATE.autoTried) {
toggleNativeFS();
STATE.autoTried = true;
}
}, { capture: true, once: true });
// Press "T" to toggle fullscreen manually.
document.addEventListener('keydown', (event) => {
const activeTag = document.activeElement?.tagName || '';
if (event.key.toLowerCase() === 't' && !['INPUT', 'TEXTAREA'].includes(activeTag)) {
STATE.userExited = false;
toggleNativeFS(true);
}
});
const observer = new MutationObserver(() => {
if (!STATE.autoTried && getFSButton()) {
toggleNativeFS();
STATE.autoTried = true;
}
syncFullscreenState();
});
observer.observe(document.body, { childList: true, subtree: true });
setInterval(syncFullscreenState, FULLSCREEN_STATE_POLL_MS);
syncFullscreenState();
})();