kittie / Classic Roblox Groups Layout Restorer

// ==UserScript==
// @name         Classic Roblox Groups Layout Restorer
// @namespace    https://openuserjs.org/users/kittie
// @author       kittie
// @homepage     https://www.roblox.com/users/4424578847/profile
// @version      0.6.5
// @copyright 2026, kittie (https://openuserjs.org/users/kittie)
// @license      MIT
// @match        *://www.roblox.com/groups/*
// @match        *://www.roblox.com/communities/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.innerHTML = `
        .group-details {
            min-height: 650px !important;
            padding-bottom: 60px !important;
        }

        .group-header {
            background: transparent !important;
            border: none !important;
            padding: 0 0 15px 0 !important;
            position: relative !important;
        }

        /* 2024-Style Theme-Adaptive Dropdown Menu */
        .classic-menu-container {
            position: absolute;
            top: 0;
            right: 0;
        }
        .classic-dots-btn {
            background: transparent;
            border: none;
            cursor: pointer;
            padding: 6px 12px;
            font-size: 20px;
            color: var(--text-color, inherit);
            opacity: 0.7;
            border-radius: 8px;
            transition: opacity 0.2s, background 0.2s;
        }
        .classic-dots-btn:hover {
            opacity: 1;
            background: rgba(128, 128, 128, 0.15);
        }
        .classic-dropdown-menu {
            display: none;
            position: absolute;
            right: 0;
            top: 40px;
            background: var(--background-secondary, var(--card-background, #ffffff));
            border: 1px solid var(--input-border, rgba(0, 0, 0, 0.15));
            border-radius: 12px;
            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
            list-style: none;
            padding: 8px;
            margin: 0;
            z-index: 1000;
            min-width: 190px;
        }
        .classic-dropdown-menu.show {
            display: block;
        }
        .classic-dropdown-item {
            padding: 10px 14px;
            font-size: 14px;
            font-weight: 400;
            color: var(--text-color, #111);
            cursor: pointer;
            text-decoration: none;
            display: block;
            white-space: nowrap;
            border-radius: 8px;
            transition: background 0.15s ease;
        }
        .classic-dropdown-item:hover {
            background: var(--state-hover, rgba(128, 128, 128, 0.12));
        }

        /* Tabs Frame */
        .rbx-tabs-horizontal {
            display: block !important;
            border-bottom: 1px solid var(--input-border, rgba(128, 128, 128, 0.3)) !important;
            margin: 18px 0 35px 0 !important;
            padding-top: 8px !important;
        }
        .rbx-tabs-horizontal ul#horizontal-tabs {
            display: flex !important;
            flex-direction: row !important;
            list-style: none !important;
            padding: 0 !important;
            margin: 0 !important;
            width: 100% !important;
        }
        .rbx-tabs-horizontal ul#horizontal-tabs li {
            flex: 1 !important;
            text-align: center !important;
            margin-right: 0 !important;
            padding-top: 8px !important;
            padding-bottom: 22px !important;
            cursor: pointer !important;
            font-size: 15px;
            color: var(--text-secondary, #555) !important;
            opacity: 0.8;
            transition: opacity 0.2s ease, color 0.2s ease;
        }
        .rbx-tabs-horizontal ul#horizontal-tabs li:hover {
            opacity: 1;
            color: var(--text-color, #000) !important;
        }
        .rbx-tabs-horizontal ul#horizontal-tabs li.active {
            border-bottom: 2px solid var(--text-color, #000) !important;
            color: var(--text-color, #000) !important;
            opacity: 1 !important;
            font-weight: 500 !important;
        }

        .classic-description-section {
            background: transparent !important;
            border: none !important;
            padding: 0 !important;
            margin-bottom: 25px !important;
        }
        .classic-description-section .container-header {
            border: none !important;
            padding-left: 0 !important;
            margin-bottom: 8px !important;
        }
        .classic-description-section h2 {
            font-size: 18px !important;
            font-weight: bold !important;
            color: var(--text-color, inherit) !important;
            margin: 0 !important;
        }
        .classic-description-text {
            background: transparent !important;
            border: none !important;
            padding: 0 !important;
            color: var(--text-color, inherit) !important;
            font-size: 14px;
        }

        .classic-group-btn {
            background: transparent !important;
            color: var(--text-color, inherit) !important;
            border: 1px solid var(--input-border, rgba(0, 0, 0, 0.3)) !important;
            padding: 6px 18px !important;
            font-weight: 400 !important;
            border-radius: 8px !important;
            cursor: pointer !important;
            font-size: 13px !important;
            display: inline-block !important;
            transition: background 0.2s, border-color 0.2s;
        }
        .classic-group-btn:hover {
            background: rgba(128, 128, 128, 0.15) !important;
        }

        .classic-members-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
            gap: 15px;
            padding: 10px 0;
            min-height: 150px;
        }
        .classic-member-card {
            text-align: center;
            text-decoration: none;
        }
        .classic-member-card img {
            width: 80px;
            height: 80px;
            border-radius: 50%;
            border: 1px solid var(--input-border, #d0d0d0);
            background: var(--background-secondary, #f2f2f2);
        }
        .classic-member-name {
            display: block;
            font-size: 12px;
            margin-top: 5px;
            color: var(--text-color, inherit);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .group-tab-content {
            min-height: 620px;
            color: var(--text-color, inherit);
        }
    `;
    document.head.appendChild(style);

    function getGroupIdFromUrl() {
        const match = window.location.pathname.match(/(?:groups|communities)\/(\d+)/);
        return match ? match[1] : null;
    }

    function getCurrentUserId() {
        try {
            if (window.Roblox && window.Roblox.CurrentUser && window.Roblox.CurrentUser.userId) {
                return String(window.Roblox.CurrentUser.userId);
            }
        } catch(e) {}

        const meta = document.querySelector('meta[name="user-data"]');
        if (meta && meta.dataset.userid) {
            return String(meta.dataset.userid);
        }

        const profileLink = document.querySelector('.rbx-menu-item[href*="/users/"]');
        if (profileLink) {
            const match = profileLink.getAttribute('href').match(/\/users\/(\d+)/);
            if (match) return match[1];
        }
        return null;
    }

    async function robloxFetch(url, options = {}) {
        options.credentials = 'include';
        let res = await fetch(url, options);
        if (res.status === 403) {
            const token = res.headers.get('x-csrf-token');
            if (token) {
                options.headers = options.headers || {};
                options.headers['x-csrf-token'] = token;
                res = await fetch(url, options);
            }
        }
        return res;
    }

    async function applyClassicLayout() {
        const container = document.querySelector('.group-details');
        const groupId = getGroupIdFromUrl();

        if (container && groupId && !container.dataset.classicRestored) {
            container.dataset.classicRestored = "true";

            container.innerHTML = `
                <div class="section-content">
                    <div class="group-header">
                        <div class="group-image" style="background: rgba(0,0,0,0.05); min-height: 150px;"></div>
                        <div class="group-caption">
                            <div class="group-title">
                                <h1 class="group-name" style="color: var(--text-color, inherit);">Loading group data...</h1>
                            </div>
                        </div>
                    </div>
                </div>
            `;

            try {
                const [groupRes, thumbRes] = await Promise.all([
                    fetch(`https://groups.roblox.com/v1/groups/${groupId}`),
                    fetch(`https://thumbnails.roblox.com/v1/groups/icons?groupIds=${groupId}&size=150x150&format=Png&isCircular=false`)
                ]);

                const groupData = await groupRes.json();
                const thumbData = await thumbRes.json();

                const groupName = groupData.name || "Unknown Group";
                const descriptionText = groupData.description || "No description provided.";
                const memberCount = groupData.memberCount ? groupData.memberCount.toLocaleString() : "0";

                const ownerName = groupData.owner ? groupData.owner.username : "Roblox";
                const ownerId = groupData.owner ? groupData.owner.userId : null;
                const ownerLink = ownerId ? `https://www.roblox.com/users/${ownerId}/profile` : "#";
                const groupImg = thumbData.data && thumbData.data.length > 0 ? thumbData.data[0].imageUrl : "";

                let membersHTML = `<div style="text-align: center; opacity: 0.7; padding: 25px;">Members list unavailable.</div>`;

                const userId = getCurrentUserId();
                let isMember = false;
                let canConfigure = false;
                let isOwner = false;

                if (ownerId && userId && String(ownerId) === String(userId)) {
                    isOwner = true;
                    canConfigure = true;
                }

                if (userId) {
                    try {
                        const userRoleRes = await robloxFetch(`https://groups.roblox.com/v1/users/${userId}/groups/roles`);
                        if (userRoleRes.ok) {
                            const userRoleData = await userRoleRes.json();
                            const groupMembership = userRoleData.data?.find(item => String(item.group.id) === String(groupId));

                            if (groupMembership) {
                                isMember = true;
                                if (groupMembership.role && groupMembership.role.rank === 255) {
                                    canConfigure = true;
                                }
                            }
                        }

                        if (!canConfigure && !isOwner) {
                            const permissionsRes = await robloxFetch(`https://groups.roblox.com/v1/groups/${groupId}/users/${userId}/permissions`);
                            if (permissionsRes.ok) {
                                const permData = await permissionsRes.json();
                                if (permData && (permData.groupChanges || permData.manageLayout || permData.inviteMembers)) {
                                    canConfigure = true;
                                }
                            }
                        }
                    } catch (e) {
                        console.error("Permission check error:", e);
                    }
                }

                let dropdownItemsHTML = '';
                if (canConfigure) {
                    dropdownItemsHTML += `<a class="classic-dropdown-item" href="https://www.roblox.com/groups/configure?id=${groupId}" target="_blank">Configure Community</a>`;
                }

                if (isMember && !isOwner) {
                    dropdownItemsHTML += `<li class="classic-dropdown-item" id="menu-leave-group">Leave Community</li>`;
                }

                dropdownItemsHTML += `<li class="classic-dropdown-item" id="menu-copy-link">Copy link</li>`;
                dropdownItemsHTML += `<li class="classic-dropdown-item" id="menu-report">Report abuse</li>`;

                const membershipActionHTML = isMember
                    ? ''
                    : `<div style="margin-top: 10px;"><button id="group-action-btn" class="classic-group-btn">Join Group</button></div>`;

                try {
                    const membersRes = await fetch(`https://groups.roblox.com/v1/groups/${groupId}/users?limit=12&sortOrder=Asc`, { credentials: 'include' });
                    if (membersRes.ok) {
                        const membersData = await membersRes.json();
                        if (membersData.data && membersData.data.length > 0) {
                            const userIds = membersData.data.map(m => m.user.userId);
                            const headshotRes = await fetch(`https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds=${userIds.join(',')}&size=150x150&format=Png&isCircular=true`);
                            const headshotData = await headshotRes.json();
                            const headshots = {};
                            headshotData.data?.forEach(h => { headshots[h.targetId] = h.imageUrl; });

                            membersHTML = `<div class="classic-members-grid">`;
                            membersData.data.forEach(m => {
                                const mId = m.user.userId;
                                const mName = m.user.username;
                                const thumbUrl = headshots[mId] || "";
                                membersHTML += `
                                    <a href="https://www.roblox.com/users/${mId}/profile" class="classic-member-card">
                                        <img src="${thumbUrl}" alt="${mName}" />
                                        <span class="classic-member-name">${mName}</span>
                                    </a>
                                `;
                            });
                            membersHTML += `</div>`;
                        }
                    }
                } catch (err) {
                    console.error("Members fetch error:", err);
                }

                container.innerHTML = `
                    <div class="section-content">
                        <div class="group-header">
                            <div class="classic-menu-container">
                                <button class="classic-dots-btn" id="classic-menu-toggle" title="More">•••</button>
                                <ul class="classic-dropdown-menu" id="classic-dropdown">
                                    ${dropdownItemsHTML}
                                </ul>
                            </div>
                            <div class="group-image">
                                <span class="thumbnail-2d-container">
                                    <img src="${groupImg}" alt="${groupName}" />
                                </span>
                            </div>
                            <div class="group-caption">
                                <div class="group-title">
                                    <h1 class="group-name" style="color: var(--text-color, inherit);">${groupName}</h1>
                                    <div class="group-owner text-date-hint">
                                        <span>By</span> <a class="text-link" href="${ownerLink}">${ownerName}</a>
                                    </div>
                                </div>
                                <div class="group-info">
                                    <ul class="group-stats">
                                        <li class="group-members">
                                            <span class="font-header-2" style="color: var(--text-color, inherit);">${memberCount}</span>
                                            <div class="text-label font-caption-header">Members</div>
                                        </li>
                                    </ul>
                                    ${membershipActionHTML}
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="rbx-tab-location">
                        <div class="rbx-tabs-horizontal">
                            <ul id="horizontal-tabs" class="nav nav-tabs" role="tablist">
                                <li id="tab-about" class="rbx-tab group-tab active" data-target="section-about">About</li>
                                <li id="tab-events" class="rbx-tab group-tab" data-target="section-events">Events</li>
                                <li id="tab-store" class="rbx-tab group-tab" data-target="section-store">Store</li>
                                <li id="tab-affiliates" class="rbx-tab group-tab" data-target="section-affiliates">Affiliates</li>
                            </ul>
                        </div>
                    </div>

                    <div id="section-about" class="group-tab-content">
                        <div class="classic-description-section">
                            <div class="container-header">
                               <h2>Description</h2>
                            </div>
                            <div class="classic-description-text">
                                <p class="text-body" style="white-space: pre-wrap; margin: 0;">${descriptionText}</p>
                            </div>
                        </div>

                        <div class="section members-section" style="background: transparent; border: none; padding: 0;">
                            <div class="container-header" style="border: none; padding-left: 0;">
                                <h2>Members</h2>
                            </div>
                            <div class="group-section-content" style="padding: 0;">
                                ${membersHTML}
                            </div>
                        </div>
                    </div>

                    <div id="section-events" class="group-tab-content" style="display:none;">
                        <div style="padding: 20px 0; text-align: center; opacity: 0.7;">No events available.</div>
                    </div>

                    <div id="section-store" class="group-tab-content" style="display:none;">
                        <div style="padding: 20px 0; text-align: center; opacity: 0.7;">Group store items.</div>
                    </div>

                    <div id="section-affiliates" class="group-tab-content" style="display:none;">
                        <div style="padding: 20px 0; text-align: center; opacity: 0.7;">No affiliates found.</div>
                    </div>
                `;

                const menuToggle = container.querySelector('#classic-menu-toggle');
                const dropdownMenu = container.querySelector('#classic-dropdown');
                if (menuToggle && dropdownMenu) {
                    menuToggle.addEventListener('click', (e) => {
                        e.stopPropagation();
                        dropdownMenu.classList.toggle('show');
                    });
                    document.addEventListener('click', () => {
                        dropdownMenu.classList.remove('show');
                    });
                }

                const copyLinkBtn = container.querySelector('#menu-copy-link');
                if (copyLinkBtn) {
                    copyLinkBtn.addEventListener('click', () => {
                        navigator.clipboard.writeText(window.location.href);
                        copyLinkBtn.innerText = "Copied!";
                        setTimeout(() => { copyLinkBtn.innerText = "Copy link"; }, 2000);
                    });
                }

                const reportBtn = container.querySelector('#menu-report');
                if (reportBtn) {
                    reportBtn.addEventListener('click', () => {
                        alert("Reporting functionality redirects or opens modal per standard Roblox behavior.");
                    });
                }

                const leaveMenuBtn = container.querySelector('#menu-leave-group');
                if (leaveMenuBtn && userId) {
                    leaveMenuBtn.addEventListener('click', async () => {
                        if (!confirm("Are you sure you want to leave this community?")) return;
                        try {
                            const res = await robloxFetch(`https://groups.roblox.com/v1/groups/${groupId}/users/${userId}`, {
                                method: 'DELETE'
                            });
                            if (res.ok) {
                                window.location.reload();
                            } else {
                                alert("Failed to leave group.");
                            }
                        } catch (err) {
                            console.error("Leave error:", err);
                        }
                    });
                }

                const actionBtn = container.querySelector('#group-action-btn');
                if (actionBtn && userId) {
                    actionBtn.addEventListener('click', async () => {
                        actionBtn.innerText = "Joining...";
                        actionBtn.style.opacity = "0.5";
                        try {
                            const res = await robloxFetch(`https://groups.roblox.com/v1/groups/${groupId}/users`, {
                                method: 'POST',
                                headers: { 'Content-Type': 'application/json' },
                                body: JSON.stringify({})
                            });
                            if (res.ok) {
                                window.location.reload();
                            } else {
                                const errJson = await res.json().catch(() => ({}));
                                alert(`Failed to join group: ${errJson.message || 'Action restricted.'}`);
                                actionBtn.innerText = "Join Group";
                                actionBtn.style.opacity = "1";
                            }
                        } catch (err) {
                            console.error("Join error:", err);
                            actionBtn.innerText = "Join Group";
                            actionBtn.style.opacity = "1";
                        }
                    });
                }

                const tabs = container.querySelectorAll('#horizontal-tabs li');
                tabs.forEach(tab => {
                    tab.addEventListener('click', () => {
                        tabs.forEach(t => t.classList.remove('active'));
                        tab.classList.add('active');

                        const targetId = tab.getAttribute('data-target');
                        container.querySelectorAll('.group-tab-content').forEach(content => {
                            content.style.display = content.id === targetId ? 'block' : 'none';
                        });
                    });
                });

            } catch (err) {
                console.error("Failed to load group details:", err);
                container.innerHTML = `<div class="section-content"><p>Failed to load group details.</p></div>`;
            }
        }
    }

    const observer = new MutationObserver(() => {
        applyClassicLayout();
    });

    observer.observe(document.body, { childList: true, subtree: true });
    applyClassicLayout();
})();