Greasy Fork is available in English.

StumbleChat - Improve Chat Readability

Adds control over font type and size of chat messages

// ==UserScript==
// @name         StumbleChat - Improve Chat Readability
// @namespace    https://greasyfork.org/en/users/1362976-gitgud
// @version      1.0.2
// @description  Adds control over font type and size of chat messages
// @author       gitgud
// @match        https://stumblechat.com/room/*
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

(function() {
    'use strict';

    // Initial settings
    let currentFontSize = parseInt(localStorage.getItem('currentFontSize')) || 18;
    let isBold = localStorage.getItem('isBold') === 'true';
    let currentFontFamily = localStorage.getItem('currentFontFamily') || 'Arial, sans-serif';
    let currentChatWidth = parseInt(localStorage.getItem('currentChatWidth')) || 500;
    let systemMessagesVisible = localStorage.getItem('systemMessagesVisible') !== 'false';

    function applyStyles() {
        const chatContent = document.getElementById('chat-content');
        if (!chatContent) return;

        const messages = chatContent.querySelectorAll('.message');
        messages.forEach(message => {
            message.style.backgroundColor = '#111111';
            message.style.color = 'white';
            message.style.fontFamily = currentFontFamily + ', sans-serif';
            message.style.fontWeight = isBold ? 'bold' : 'normal';
            message.style.fontSize = `${currentFontSize}px`;
            message.style.lineHeight = '1.2';

            // Apply styles to .content class divs within .message
            const contentDivs = message.querySelectorAll('.content');
            contentDivs.forEach(contentDiv => {
                contentDiv.style.border = '1px solid #333333';
                contentDiv.style.borderRadius = '3px';
            });
        });

        const messageBlock = chatContent.querySelectorAll(':scope > .message');
        messageBlock.forEach(message => {
            message.style.border = '2px solid #333333';
            message.style.borderRadius = '3px';
        });

        // Handles auto scroll
        const scrollChatElement = document.getElementById("chat");
        const heightVariable = currentFontSize < 21 ? 150 : currentFontSize < 25 ? 250 : 500;
        const isNearBottom = (scrollChatElement.scrollTop + scrollChatElement.clientHeight) >= (scrollChatElement.scrollHeight - heightVariable);
        if (isNearBottom) {
            scrollChatElement.scrollTop = scrollChatElement.scrollHeight;
        }

        // Apply chat width
        const chatElement = document.querySelector('sc-chat');
        if (chatElement) {
            chatElement.style.width = `${currentChatWidth}px`;
        }

        // Hide or show system messages
        if (systemMessagesVisible) {
            showSystemMessages();
        } else {
            hideSystemMessages();
        }
    }

    function hideSystemMessages() {
        const elements = document.querySelectorAll('.message.system');

        elements.forEach(element => {
            if (!element.textContent.includes('Admin Announcement')) {
                element.style.display = 'none';
            }
        });
    }

    function showSystemMessages() {
        const elements = document.querySelectorAll('.message.system');
        elements.forEach(element => {
            element.style.display = ''; // Reset display
        });
    }

    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            if (mutation.type === 'childList' && mutation.addedNodes.length) {
                applyStyles();
            }
        }
    });

    applyStyles();

    // Observe for new messages being added
    const config = { childList: true, subtree: true };
    const targetNode = document.getElementById('chat');
    if (targetNode) {
        observer.observe(targetNode, config);
    }

    // Helper functions for creating elements
    const createButton = (className, clickHandler) => {
        const button = document.createElement('button');
        const svgDown = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M19.924 13.617A1 1 0 0 0 19 13h-3V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v10H5a1 1 0 0 0-.707 1.707l7 7a1 1 0 0 0 1.414 0l7-7a1 1 0 0 0 .217-1.09z" style="fill:#ff891a" data-name="Down"/></svg>';
        const svgUp = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="m19.707 9.293-7-7a1 1 0 0 0-1.414 0l-7 7A1 1 0 0 0 5 11h3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V11h3a1 1 0 0 0 .707-1.707z" style="fill:#ff891a" data-name="Up"/></svg>';
        button.classList.add('button-style', className);
        button.innerHTML = className === 'button-style--decrease' ? svgDown : svgUp;
        button.addEventListener('click', clickHandler);
        button.addEventListener('mousedown', () => {
            button.style.transform = 'scale(0.9)';
        });
        button.addEventListener('mouseup', () => {
            button.style.transform = 'scale(1)';
        });
        return button;
    };

    const createCheckboxContainer = () => {
        const container = document.createElement('div');
        container.classList.add('checkbox-container-style');
        return container;
    };

    const createCheckmark = () => {
        const checkmark = document.createElement('div');
        checkmark.classList.add('checkmark-style');
        return checkmark;
    };

    const createControls = () => {
        const controls = document.createElement('div');
        controls.style.display = 'flex';
        controls.style.alignItems = 'center';
        return controls;
    };

    const createLabel = (labelText) => {
        const label = document.createElement('div')
        label.style.color = 'white'
        label.textContent = labelText;
        return label;
    };

    const createRow = () => {
        const row = document.createElement('div');
        row.classList.add('row-style');
        return row;
    };

    // Settings menu container
    const container = document.createElement('div');
    container.classList.add('settings-container-style')

    // Chat width row
    const chatWidthRow = createRow();
    const chatWidthLabel = createLabel('Chat Width:');
    const chatWidthControls = createControls();

    const decreaseWidthButton = createButton('button-style--decrease', () => {
        const chatElement = document.querySelector('sc-chat');
        if (chatElement) {
            const currentWidth = parseInt(chatElement.style.width) || 500;
            if (currentWidth >= 550) {
                currentChatWidth = currentWidth - 50;
                localStorage.setItem('currentChatWidth', currentChatWidth);
                applyStyles();
            }
        }
    });

    const increaseWidthButton = createButton('button-style--increase', () => {
        const chatElement = document.querySelector('sc-chat');
        if (chatElement) {
            const currentWidth = parseInt(chatElement.style.width) || 500;
            if (currentWidth >= 500 && currentWidth < 1000) {
                currentChatWidth = currentWidth + 50;
                localStorage.setItem('currentChatWidth', currentChatWidth);
                applyStyles();
            }
        }
    });

    chatWidthControls.appendChild(decreaseWidthButton);
    chatWidthControls.appendChild(increaseWidthButton);

    chatWidthRow.appendChild(chatWidthLabel);
    chatWidthRow.appendChild(chatWidthControls);

    // Font size row
    const fontSizeRow = createRow()
    const fontSizeLabel = createLabel('Font Size:');
    const fontSizeControls = createControls();

    const decreaseFontButton = createButton('button-style--decrease', () => {
        if (currentFontSize > 12) {
            currentFontSize -= 2;
            localStorage.setItem('currentFontSize', currentFontSize);
            applyStyles();
        }
    });

    const increaseFontButton = createButton('button-style--increase', () => {
        if (currentFontSize < 28) {
            currentFontSize += 2;
            localStorage.setItem('currentFontSize', currentFontSize);
            applyStyles();
        }
    });

    fontSizeControls.appendChild(decreaseFontButton);
    fontSizeControls.appendChild(increaseFontButton);

    fontSizeRow.appendChild(fontSizeLabel);
    fontSizeRow.appendChild(fontSizeControls);

    // Font family row
    const fontFamilyRow = createRow();

    const fontFamilyDropdown = document.createElement('select');
    fontFamilyDropdown.classList.add('dropdown-style')

    const fontOptions = [
        'Arial',
        'Courier New',
        'Georgia',
        'Helvetica',
        'Lucida Sans',
        'Palatino',
        'Tahoma',
        'Times New Roman',
        'Trebuchet MS',
        'Verdana',
    ];

    fontOptions.forEach(font => {
        const option = document.createElement('option');
        option.value = font;
        option.textContent = font;
        fontFamilyDropdown.appendChild(option);
    });

    fontFamilyDropdown.value = currentFontFamily.split(',')[0];
    fontFamilyDropdown.addEventListener('change', () => {
        currentFontFamily = fontFamilyDropdown.value + ', sans-serif';
        localStorage.setItem('currentFontFamily', currentFontFamily);
        applyStyles();
    });

    fontFamilyRow.appendChild(fontFamilyDropdown);

    // Bold font row
    const boldFontRow = createRow();
    const boldLabel = createLabel('Bold Font:');
    const boldCheckboxContainer = createCheckboxContainer();

    const boldCheckbox = document.createElement('input');
    boldCheckbox.classList.add('checkbox-style');
    boldCheckbox.type = 'checkbox';
    boldCheckbox.checked = isBold;
    boldCheckbox.addEventListener('change', () => {
        isBold = boldCheckbox.checked;
        localStorage.setItem('isBold', isBold);
        applyStyles();
        boldCheckmark.style.display = isBold ? 'block' : 'none';
    });

    const boldCheckmark = createCheckmark();
    boldCheckmark.style.display = isBold ? 'block' : 'none';

    boldCheckboxContainer.appendChild(boldCheckbox);
    boldCheckboxContainer.appendChild(boldCheckmark);

    boldFontRow.appendChild(boldLabel);
    boldFontRow.appendChild(boldCheckboxContainer);

    // System messages row
    const systemMessagesRow = createRow();
    const systemMessagesLabel = createLabel('Hide System Msgs:');
    const systemMessagesCheckboxContainer = createCheckboxContainer();

    const systemMessagesCheckbox = document.createElement('input');
    systemMessagesCheckbox.classList.add('checkbox-style');
    systemMessagesCheckbox.type = 'checkbox';
    systemMessagesCheckbox.checked = !systemMessagesVisible;
    systemMessagesCheckbox.addEventListener('change', () => {
        systemMessagesVisible = !systemMessagesCheckbox.checked;
        localStorage.setItem('systemMessagesVisible', systemMessagesVisible);
        applyStyles();
        systemMessagesCheckmark.style.display = systemMessagesCheckbox.checked ? 'block' : 'none';
    });

    const systemMessagesCheckmark = createCheckmark();
    systemMessagesCheckmark.style.display = systemMessagesCheckbox.checked ? 'block' : 'none';

    systemMessagesCheckboxContainer.appendChild(systemMessagesCheckbox);
    systemMessagesCheckboxContainer.appendChild(systemMessagesCheckmark);

    systemMessagesRow.appendChild(systemMessagesLabel);
    systemMessagesRow.appendChild(systemMessagesCheckboxContainer);

    // Append options to menu container
    container.appendChild(chatWidthRow);
    container.appendChild(fontSizeRow);
    container.appendChild(fontFamilyRow);
    container.appendChild(boldFontRow);
    container.appendChild(systemMessagesRow);

    // Create the settings icon
    const settingsIcon = document.createElement('div');
    settingsIcon.style.position = 'fixed';
    settingsIcon.style.bottom = '10px';
    settingsIcon.style.right = '10px';
    settingsIcon.style.zIndex = '1001';
    settingsIcon.style.cursor = 'pointer';
    settingsIcon.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="30" height="30" viewBox="0 0 256 256" xml:space="preserve">
                <defs>
                </defs>
                <g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)">
                    <circle cx="45" cy="45" r="45" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: #ff891a; fill-rule: nonzero; opacity: 1;" transform="  matrix(1 0 0 1 0 0) "/>
                    <path d="M 37.858 36.563 l 15.58 15.58 c 4.443 -1.611 9.609 -0.649 13.171 2.913 c 3.694 3.694 4.604 9.115 2.735 13.663 l -7.126 -7.126 c -1.881 -1.881 -4.958 -1.881 -6.838 0 c -1.881 1.881 -1.881 4.958 0 6.838 l 7.126 7.126 c -4.548 1.869 -9.969 0.959 -13.663 -2.735 c -3.562 -3.562 -4.524 -8.728 -2.913 -13.171 l -15.58 -15.58 c -4.443 1.611 -9.609 0.649 -13.171 -2.913 c -3.694 -3.694 -4.604 -9.115 -2.735 -13.663 l 7.126 7.126 c 1.881 1.881 4.958 1.881 6.838 0 c 1.881 -1.881 1.881 -4.958 0 -6.838 l -7.126 -7.126 c 4.548 -1.869 9.969 -0.959 13.663 2.735 C 38.506 26.954 39.469 32.12 37.858 36.563 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: #ff891a; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
                    <path d="M 41.072 33.349 l 15.58 15.58 c 4.443 -1.611 9.609 -0.649 13.171 2.913 c 3.694 3.694 4.604 9.115 2.735 13.663 l -7.126 -7.126 c -1.881 -1.881 -4.958 -1.881 -6.838 0 c -1.881 1.881 -1.881 4.958 0 6.838 l 7.126 7.126 c -4.548 1.869 -9.969 0.959 -13.663 -2.735 c -3.562 -3.562 -4.524 -8.728 -2.913 -13.171 l -15.58 -15.58 c -4.443 1.611 -9.609 0.649 -13.171 -2.913 c -3.694 -3.694 -4.604 -9.115 -2.735 -13.663 l 7.126 7.126 c 1.881 1.881 4.958 1.881 6.838 0 c 1.881 -1.881 1.881 -4.958 0 -6.838 l -7.126 -7.126 c 4.548 -1.869 9.969 -0.959 13.663 2.735 C 41.721 23.74 42.683 28.906 41.072 33.349 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: #ffffff; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
                </g>
            </svg>
        `;

    // Function to reset settings to default
    function resetSettings() {
        currentFontSize = 18;
        isBold = false;
        currentFontFamily = 'Arial';
        currentChatWidth = 500;
        systemMessagesVisible = true;

        localStorage.setItem('currentFontSize', currentFontSize);
        localStorage.setItem('isBold', isBold);
        localStorage.setItem('currentFontFamily', currentFontFamily);
        localStorage.setItem('currentChatWidth', currentChatWidth);
        localStorage.setItem('systemMessagesVisible', systemMessagesVisible);

        // Update UI elements
        fontFamilyDropdown.value = 'Arial';
        boldCheckbox.checked = false;
        boldCheckmark.style.display = 'none';
        systemMessagesCheckbox.checked = false;
        systemMessagesCheckmark.style.display = 'none';

        applyStyles();
        showSystemMessages();
    }

    // Handle settings icon click
    settingsIcon.addEventListener('click', (event) => {
        if (event.ctrlKey || event.metaKey) {
            resetSettings();
        } else {
            if (container.style.display === 'none') {
                container.style.display = 'flex';
                settingsIcon.classList.add('selected-font-icon');
            } else {
                container.style.display = 'none';
                settingsIcon.classList.remove('selected-font-icon');
            }
        }
    });

    // Close the settings container when clicking outside
    document.addEventListener('click', (event) => {
        if (!container.contains(event.target) && !settingsIcon.contains(event.target)) {
            container.style.display = 'none';
            settingsIcon.classList.remove('selected-font-icon');
        }
    });

    // CSS
    const style = document.createElement('style');
    style.innerHTML = `
            .selected-font-icon {
                border: 2px solid #ff891a;
                border-radius: 50%;
                background-color: #ff891a;
            }
            .selected-font-icon svg path {
                fill: #ffffff;
            }
            .button-style {
                background-color: transparent;
                color: white;
                border: 2px solid #ff891a;
                border-radius: 5px 0 0 5px;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                width: 30px;
                height: 30px;
                transition: transform 0.1s ease-in-out;
            }
            .button-style--decrease {
                border-radius: 5px 0 0 5px;
            }
            .button-style--increase {
                border-radius: 0 5px 5px 0;
            }
            .checkbox-container-style {
                position: relative;
                width: 20px;
                height: 20px;
                border: 2px solid #ff891a;
                border-radius: 5px;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .checkbox-style {
                opacity: 0;
                position: absolute;
                width: 100%;
                height: 100%;
                cursor: pointer;
            }
            .checkmark-style {
                width: 10px;
                height: 10px;
                background-color: #ff891a;
                border-radius: 50%;
            }
            .dropdown-style{
                background-color: #333;
                color: white;
                border: 2px solid #ff891a;
                border-radius: 5px;
                padding: 5px;
                cursor: pointer;
            }
            .row-style {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 10px;
                width: 100%;
            }
            .settings-container-style{
                position: fixed;
                bottom: 45px;
                right: 30px;
                z-index: 1000;
                display: none;
                flex-direction: column;
                align-items: flex-start;
                background-color: #222;
                padding: 10px;
                border-radius: 10px;
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), 0 6px 20px rgba(0, 0, 0, 0.5);
                border: 2px solid #141414;
            }
        `;
    document.head.appendChild(style);

    document.body.appendChild(container);
    document.body.appendChild(settingsIcon);
})();