YouTube Remove Important Notifications

Sorts all notifications chronologically and hides section titles & borders.

// ==UserScript==
// @name            YouTube Remove Important Notifications
// @namespace       https://github.com/crazyrabbit0
// @version         1.0
// @description     Sorts all notifications chronologically and hides section titles & borders.
// @author          CrazyRabbit
// @match           http://*.youtube.com/*
// @match           https://*.youtube.com/*
// @icon            https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant           none
// @license         GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// @copyright       2023+, CrazyRabbit (https://github.com/crazyrabbit0)
// @homepageURL     https://github.com/crazyrabbit0/UserScripts
// @supportURL      https://github.com/crazyrabbit0/UserScripts/issues/new?assignees=crazyrabbit0&labels=help+wanted&template=&title=YouTube%20Remove%20Important%20Notifications%20-%20Issue
// @run-at          document-start
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    // YouTube DOM structure selectors
    const NOTIFICATION_SELECTORS = {
        button: 'ytd-notification-topbar-button-renderer',
        panel: 'ytd-multi-page-menu-renderer #sections',
        section: 'yt-multi-page-menu-section-renderer',
        title: '#section-title',
        items: '#items',
        item: 'ytd-notification-renderer',
        time: 'div.metadata yt-formatted-string:last-of-type'
    };

    // Constants for timing
    const BUTTON_POLL_INTERVAL = 200;    // ms - interval for checking if the button is available
    const OBSERVER_RETRY_DELAY = 200;    // ms - delay before retrying to setup observer
    const MERGE_DEBOUNCE_DELAY = 200;    // ms - delay for debouncing merge operations

    // Global variables to manage the MutationObserver and debounce timer
    let notificationObserver = null;
    let debounceTimer = null;

    /**
     * Parses YouTube's time element (e.g., "5 hours ago", "2 days ago") into minutes for chronological sorting
     * Handles common time units: seconds, minutes, hours, days, weeks, months, years
     * Returns Infinity for invalid time strings to ensure they appear at the end of sorted results
     * @param {string} timeElement - The time element to parse (e.g., "5 hours ago")
     * @returns {number} Notification time in minutes or Infinity if parsing fails
     */
    const parseTimeToMinutes = (timeElement) => {
        if (!timeElement) return Infinity;

        // Clean and split the string into parts
        const string = timeElement?.textContent.toLowerCase();
        const parts = string.split(' ');

        // Parse the numeric value
        const value = parseInt(parts[0], 10);
        if (isNaN(value)) return Infinity;

        // Normalize the unit by removing 's' if it's plural
        const unit = parts[1].endsWith('s') ? parts[1].slice(0, -1) : parts[1];

        // Lookup map for converting various time units to minutes
        const timeMultipliers = new Map([
            ['second', 1 / 60],    // Convert seconds to fractional minutes
            ['minute', 1],       // Minutes stay as-is
            ['hour', 60],        // Hours to minutes
            ['day', 1440],       // Days to minutes (24 * 60)
            ['week', 10080],     // Weeks to minutes (7 * 24 * 60)
            ['month', 43200],    // Months to minutes (30 * 24 * 60)
            ['year', 525600]     // Years to minutes (365 * 24 * 60)
        ]);

        // Direct lookup for the normalized unit
        const multiplier = timeMultipliers.get(unit);
        return multiplier !== undefined ? value * multiplier : Infinity;
    };

    /**
     * Sorts all notifications chronologically across the 'Important' and 'Main' sections
     * Also hides the section titles.
     * @param {HTMLElement} panel - The container element holding notification sections
     */
    const sortNotifications = (panel) => {
        notificationObserver.disconnect(); // Disconnect to prevent recursive triggers

        const sections = panel.querySelectorAll(NOTIFICATION_SELECTORS.section);
        if (sections.length < 2) return; // Need both 'Important' and 'Main' sections to proceed

        // Collect all notifications and their parsed times, while hiding section titles and borders
        const notificationsToSort = [];
        sections.forEach(section => {
            // Hide section titles and border
            section.querySelector(NOTIFICATION_SELECTORS.title).style.display = 'none';
            section.style.border = 'none';

            const items = section.querySelector(NOTIFICATION_SELECTORS.items);
            const notifications = items.querySelectorAll(NOTIFICATION_SELECTORS.item);
            if (!notifications.length) return; // No notifications found

            // Gather all notifications
            for (const notification of notifications) {
                const timeElement = notification.querySelector(NOTIFICATION_SELECTORS.time);

                // Create an object with the DOM element and its parsed time
                notificationsToSort.push({
                    element: notification,
                    time: parseTimeToMinutes(timeElement)
                });
            }
        });

        // Sort notifications by time (newest first)
        notificationsToSort.sort((a, b) => a.time - b.time);

        // Extract only the DOM elements from the sorted notifications
        const sortedNotifications = notificationsToSort.map(notification => notification.element);

        // Add notifications back into sections
        sections.forEach((section, index) => {
            const items = section.querySelector(NOTIFICATION_SELECTORS.items);

            // For the 'Important' section (index 0), add only the first two notifications
            // For the 'Main' section (index 1), add the remaining notifications
            const notificationsToAdd = (index === 0) ? sortedNotifications.slice(0, 2) : sortedNotifications.slice(2);
            items.prepend(...notificationsToAdd);
        });
    };

    /**
     * Creates and attaches a MutationObserver to monitor changes in the notification container
     * Uses debouncing to prevent excessive merge operations when multiple notifications are added rapidly
     * The observer watches for childList changes (new notifications being added) and triggers merge with a delay
     */
    const setupNotificationObserver = () => {
        const panel = document.querySelector(NOTIFICATION_SELECTORS.panel);
        // Retry after configured delay, if the notification panel isn't rendered yet
        if (!panel) {
            setTimeout(setupNotificationObserver, OBSERVER_RETRY_DELAY);
            return;
        }

        // Check if any mutation added nodes, then trigger merge with debounce
        notificationObserver = new MutationObserver((mutations) => {
            if (!mutations.some(mutation => mutation.addedNodes.length > 0)) return; // No relevant changes

            // Debounce ensures merge only runs once after a batch of changes complete
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => sortNotifications(panel), MERGE_DEBOUNCE_DELAY);
        });

        // Observe section childList changes (element additions/removals)
        notificationObserver.observe(panel, {
            childList: true,
            subtree: false
        });
    };

    /**
     * Polls the DOM to find the notification button and attaches a click listener.
     */
    const initialize = () => {
        const buttonInterval = setInterval(() => {
            const button = document.querySelector(NOTIFICATION_SELECTORS.button);
            if (!button) return; // No button found

            clearInterval(buttonInterval);

            // Attach the click listener to initialize the notification observer
            button.addEventListener('click', () => {
                if (notificationObserver) notificationObserver.disconnect(); // Disconnect any existing observer
                setupNotificationObserver();
            });
        }, BUTTON_POLL_INTERVAL);
    };

    // Start polling for the notification button
    initialize();

})();