// ==UserScript==
// @name fix reddit chat on mobile
// @namespace https://gist.github.com/nuckle/92100273f64a8d18d0010082fff0b587
// @version 1.0
// @description Simple fix to make reddit chats usable in browsers on mobile devices
// @author nuckle
// @match *://chat.reddit.com/*
// @grant unsafeWindow
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const shadowRoots = new Set();
const listeners = new WeakMap();
const originalAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function () {
const shadowRoot = originalAttachShadow.apply(this, arguments);
// Remove duplicates
let isDeleted = false;
// Clean up
shadowRoots.forEach((shadowRootSet) => {
if (
shadowRootSet.host.innerHTML === shadowRoot.host.innerHTML &&
shadowRootSet.host.nodeName === shadowRoot.host.nodeName &&
shadowRootSet.host.dataset.changed !== 'true'
) {
shadowRoots.delete(shadowRootSet);
isDeleted = true;
}
});
if (!isDeleted) shadowRoots.add(shadowRoot);
return shadowRoot;
};
function debounce(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => func(...args), delay);
};
}
function updateEventListener(element, eventType, callback) {
if (
!element ||
typeof callback !== 'function' ||
typeof eventType !== 'string'
)
return;
const boundCallback = callback.bind(element);
if (listeners.has(element)) {
const existingListeners = listeners.get(element);
if (
existingListeners.some(
(listener) => listener.callback.toString() === callback.toString(),
)
) {
// Listener already registered.
return;
}
}
// Cleanup any duplicate listeners
element.removeEventListener(eventType, boundCallback);
element.addEventListener(eventType, boundCallback);
// Track the listener for later removal
if (!listeners.has(element)) {
listeners.set(element, []);
}
listeners.get(element).push({ eventType, callback });
}
function adjustTextareaHeight(textarea) {
setTimeout(function () {
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
}, 1);
}
function updateTextareaHeight(textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
}
function createCustomDesktopStyleClass(shadow, className, styles) {
const styleElement = document.createElement('style');
let styleString = '@media (min-width: 768px) {';
styleString += `.${className} {`;
for (const [property, value] of Object.entries(styles)) {
styleString += `${property}: ${value}!important; `;
}
styleString += '}}';
styleElement.textContent = styleString;
shadow.appendChild(styleElement);
}
function createCustomMobileStyleClass(shadow, className, styles) {
const styleElement = document.createElement('style');
let styleString = '@media (max-width: 768px) {';
styleString += `.${className} {`;
for (const [property, value] of Object.entries(styles)) {
styleString += `${property}: ${value}!important; `;
}
styleString += '}}';
styleElement.textContent = styleString;
shadow.appendChild(styleElement);
}
// Styles for div.container (under shadow DOM)
const hiddenChatStyles = {
'grid-template-columns': 'auto 0',
};
const visibleChatStyles = {
'grid-template-columns': '0 auto',
};
const hiddenStyles = {
display: 'none',
};
const containerVisibleClass = 'container--navbar-visible';
const containerHiddenClass = 'container--navbar-hidden';
const toggleBtnClass = 'custom-js-hide-button';
const hiddenElClass = 'hidden';
const toggleBtnText = 'Toggle';
let existingMainContainer = null;
// A function to track if shadowRoot was changed
// (we don't want to delete changed shadowRoots)
function setChanged(shadowRootHost) {
shadowRootHost.dataset.changed = true;
}
function applyStyles() {
const toggleChatWindow = () => {
if (existingMainContainer) {
const isVisible = existingMainContainer?.classList.contains(
containerVisibleClass,
);
const chatOverlay = existingMainContainer?.querySelector(
'rs-room-overlay-manager',
);
const chatThreads = existingMainContainer?.querySelector('rs-threads-view');
// To avoid a blank screen
if (chatOverlay || chatThreads) {
// To prevent 'Read more' messages when chat overlay has 0 width
chatOverlay?.classList.toggle(hiddenElClass, !isVisible);
existingMainContainer?.classList.toggle(containerHiddenClass, isVisible);
existingMainContainer?.classList.toggle(containerVisibleClass, !isVisible);
}
}
};
const showChatWindow = () => {
if (existingMainContainer) {
existingMainContainer?.classList.remove(containerHiddenClass);
existingMainContainer?.classList.add(containerVisibleClass);
const chatOverlay = existingMainContainer?.querySelector(
'rs-room-overlay-manager',
);
chatOverlay?.classList.add(hiddenElClass);
}
};
const hideChatWindow = () => {
if (existingMainContainer) {
existingMainContainer?.classList.add(containerHiddenClass);
existingMainContainer?.classList.remove(containerVisibleClass);
const chatOverlay = existingMainContainer?.querySelector(
'rs-room-overlay-manager',
);
chatOverlay?.classList.remove(hiddenElClass);
}
};
const createToggleButton = (parentElement) => {
const button = document.createElement('button');
button.textContent = toggleBtnText;
button.classList.add(toggleBtnClass);
updateEventListener(button, 'click', toggleChatWindow);
parentElement.appendChild(button);
createCustomDesktopStyleClass(parentElement, toggleBtnClass, hiddenStyles);
return button;
};
shadowRoots.forEach((shadow) => {
if (!(shadow instanceof ShadowRoot)) {
return;
}
const header = shadow?.querySelector('main header.flex');
const container = shadow?.querySelector('div.container');
const createRoomBtn = shadow?.querySelector('rs-room-creation-button');
const existingBtnContainer = createRoomBtn?.parentNode;
const composerTextArea = shadow?.querySelector(
'rs-textarea-auto-size textarea',
);
const chatRoomLinks = shadow?.querySelectorAll('rs-rooms-nav-room');
// Exclude aria-label to not interact with button from Chat settings
const backBtn = shadow?.querySelector(
'main > header > button.button-small.button-plain.icon.inline-flex.text-tone-2.back-icon-display:not([aria-label])',
);
const settingsBtn = shadow?.querySelector(
'button.text-tone-2.button-small.button-plain.button.inline-flex[aria-label="Open chat settings"]',
);
const createChatBtn = shadow?.querySelector(
'a.button-plain[href="/room/create"]',
);
const cancelBtn = Array.from(
shadow.querySelectorAll('form .buttons button.button-secondary'),
).find((btn) => btn.textContent.trim() === 'Cancel');
const btnElements = shadow?.querySelectorAll(
'div.border-solid > div.flex > li.relative.list-none.mt-0[role="presentation"]',
);
const requestBtn = btnElements[0];
const threadsBtn = btnElements[1] || btnElements[0];
const startChatBtn = shadow?.querySelector(
'form div.buttons button.button-primary[type="submit"]',
);
const welcomeScreen = container?.querySelector('rs-welcome-screen');
// Avoid getting "stuck"
if (welcomeScreen) {
setTimeout(() => {
showChatWindow();
}, 300);
}
// Fix scroll "jumping" when user is entering a message
if (composerTextArea) {
setChanged(shadow.host);
composerTextArea.style.overflowY = 'auto';
const inputCallback = (e) => {
e.stopImmediatePropagation();
debounce(() => {
adjustTextareaHeight(composerTextArea);
}, 150)();
};
const focusoutCallback = () => {
updateTextareaHeight(composerTextArea);
};
updateEventListener(composerTextArea, 'input', inputCallback);
updateEventListener(composerTextArea, 'focusout', focusoutCallback);
}
// Initialize the main container if not already set
if (!existingMainContainer && container) {
setChanged(shadow.host);
existingMainContainer = container;
createCustomMobileStyleClass(
shadow,
containerVisibleClass,
hiddenChatStyles,
);
// hide overlay to fix false truncated messages
createCustomMobileStyleClass(
shadow,
'container--navbar-visible rs-room-overlay-manager',
hiddenStyles,
);
createCustomMobileStyleClass(
shadow,
containerHiddenClass,
visibleChatStyles,
);
createCustomMobileStyleClass(shadow, hiddenElClass, hiddenStyles);
if (!container?.classList.contains(containerVisibleClass)) {
container?.classList.add(containerVisibleClass);
}
}
// Create and attach toggle button in header if it doesn't exist
if (header && !header?.querySelector(`button.${toggleBtnClass}`)) {
setChanged(shadow.host);
createToggleButton(header);
}
// Create and attach toggle button in navbar if it doesn't exist
if (
createRoomBtn &&
existingBtnContainer &&
!existingBtnContainer?.querySelector(`button.${toggleBtnClass}`)
) {
setChanged(shadow.host);
createToggleButton(existingBtnContainer);
}
if (
chatRoomLinks.length > 0 ||
backBtn ||
settingsBtn ||
threadsBtn ||
requestBtn ||
createChatBtn ||
startChatBtn ||
cancelBtn
) {
function handleButtonClick(element, eventType, callback) {
if (element) {
updateEventListener(element, eventType, callback);
}
}
setChanged(shadow.host);
const showCallback = () => showChatWindow();
const hideCallback = () => hideChatWindow();
handleButtonClick(cancelBtn, 'click', showCallback);
handleButtonClick(createChatBtn, 'click', hideCallback);
handleButtonClick(settingsBtn, 'click', hideCallback);
handleButtonClick(backBtn, 'click', showCallback);
handleButtonClick(requestBtn, 'click', showCallback);
handleButtonClick(threadsBtn, 'click', hideCallback);
handleButtonClick(startChatBtn, 'click', hideCallback);
chatRoomLinks?.forEach((chatRoomLink) => {
handleButtonClick(chatRoomLink, 'click', hideCallback);
});
}
});
}
window.Element.prototype.attachShadowOri =
window.Element.prototype.attachShadow;
window.Element.prototype.attachShadow = function (obj) {
obj.mode = 'open';
applyStyles();
return this.attachShadowOri(obj);
};
})();