ChatGPT bring back date grouping

Brings back the date grouping on chatgpt.com

// ==UserScript==
// @name        ChatGPT bring back date grouping
// @version     1.1
// @author      tiramifue
// @description Brings back the date grouping on chatgpt.com
// @match       https://chatgpt.com/*
// @run-at      document-end
// @namespace   https://greasyfork.org/users/570213
// @license     Apache-2.0
// @noframes
// ==/UserScript==

// updated 2025-06-12

(function () {
    'use strict';

    function getDateGroupLabel(isoString) {
        const createdDate = new Date(isoString);
        const now = new Date();

        const msInDay = 24 * 60 * 60 * 1000;
        const daysAgo = Math.floor((now - createdDate) / msInDay);

        const createdMonth = createdDate.getMonth();
        const createdYear = createdDate.getFullYear();
        const nowMonth = now.getMonth();
        const nowYear = now.getFullYear();

        if (daysAgo === 0) return 'Today';
        if (daysAgo === 1) return 'Yesterday';
        if (daysAgo <= 6) return `${daysAgo} days ago`;
        if (daysAgo <= 13) return 'Last week';
        if (daysAgo <= 20) return '2 weeks ago';
        if (daysAgo <= 31) return 'Last month';

        const monthsAgo = (nowYear - createdYear) * 12 + (nowMonth - createdMonth);

        if (monthsAgo <= 11) return `${monthsAgo} months ago`;
        return 'Last year';
    }

    function getReactFiber(dom) {
        for (const key in dom) {
            if (key.startsWith('__reactFiber$')) return dom[key];
        }
        return null;
    }

    function extractChatInfo(fiber) {
        const c = fiber.memoizedProps?.conversation;
        return c
            ? {
            id: c.id,
            title: c.title,
            created: c.create_time,
            updated: c.update_time,
            node: fiber.stateNode
        }
        : null;
    }

    const seenIds = new Set();
    const chatList = [];

    function processNewChatNode(node) {
        const fiber = getReactFiber(node);
        if (!fiber) return;

        let current = fiber;
        while (current && !current.memoizedProps?.conversation) {
            current = current.return;
        }

        if (!current || !current.memoizedProps?.conversation) return;

        const chat = extractChatInfo(current);
        if (chat && !seenIds.has(chat.id)) {
            seenIds.add(chat.id);
            chat.group = getDateGroupLabel(chat.created);
            chat.node = node;
            chatList.push(chat);

            // console.log("New chat loaded:", {
            //     id: chat.id,
            //     title: chat.title,
            //     created: chat.created,
            //     updated: chat.updated,
            //     group: chat.group
            // });

            queueRender();
        }
    }

    function groupChatsByGroupName() {
        const groups = new Map();

        for (const chat of chatList) {
            if (!groups.has(chat.group)) groups.set(chat.group, []);
            groups.get(chat.group).push(chat);
        }

        return [...groups.entries()].sort((a, b) => {
            const aTime = new Date(a[1][0].created).getTime();
            const bTime = new Date(b[1][0].created).getTime();
            return bTime - aTime;
        });
    }

    function clearGroupedChats(aside) {
        aside.querySelectorAll('a[href^="/c/"], .__chat-group-header')
            .forEach(el => el.remove());
    }

    function renderGroupedChats(aside) {
        clearGroupedChats(aside);
        const groups = groupChatsByGroupName();

        for (const [label, chats] of groups) {
            const header = document.createElement('div');
            header.className = '__chat-group-header';
            header.textContent = label;
            header.style = `
    font-weight: normal;
    padding: 6px 10px;
    font-size: 0.85rem;
    color: #999;
    margin-top: ${aside.querySelector('.__chat-group-header') ? '12px' : '0'};
`;
            aside.appendChild(header);

            chats
                .sort((a, b) => new Date(b.created) - new Date(a.created))
                .forEach(chat => aside.appendChild(chat.node));
        }
    }

    let renderTimer = null;

    function queueRender() {
        if (renderTimer) clearTimeout(renderTimer);
        renderTimer = setTimeout(() => {
            const aside = document.querySelector('#history aside');
            if (aside) renderGroupedChats(aside);
        }, 200);
    }

    function observeChatList(aside) {
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1 && node.matches('a[href^="/c/"]')) {
                        processNewChatNode(node);
                    }
                }
            }
        });

        observer.observe(aside, { childList: true, subtree: true });

        aside.querySelectorAll('a[href^="/c/"]').forEach(processNewChatNode);
    }

    (function watchSidebar() {
        let lastAside = null;

        function setup(aside) {
            if (!aside || aside === lastAside) return;
            lastAside = aside;

            observeChatList(aside);
            renderGroupedChats(aside);
            console.log("ChatGPT grouping: sidebar attached.");
        }

        const rootObserver = new MutationObserver(() => {
            const history = document.querySelector('#history');
            if (!history) return;

            const aside = history.querySelector('aside');
            if (aside && aside !== lastAside) {
                setup(aside);
            }
        });

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

        const asideNow = document.querySelector('#history aside');
        if (asideNow) setup(asideNow);
    })();
})();