Skip to content

Instantly share code, notes, and snippets.

@nuckle
Last active December 26, 2024 17:26
Show Gist options
  • Save nuckle/92100273f64a8d18d0010082fff0b587 to your computer and use it in GitHub Desktop.
Save nuckle/92100273f64a8d18d0010082fff0b587 to your computer and use it in GitHub Desktop.
Simple user script to make reddit chats usable in browsers on mobile devices

What does this script do?

Also publised at greasyfork

Note: this script has been migrated to a browser extension format

Well, it fixes reddit chat (chat.reddit.com) for mobile devices.

Tested only for DMs (no group chats, channels or threads)

Important: may need a user-agent switcher

Without the script

without script

With the script

chat window

rooms window

// ==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);
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment