// ==UserScript== // @name GitHub - Latest // @version 1.8.0 // @description Always keep an eye on the latest activity of your favorite projects // @author Journey Over // @license MIT // @match *://github.com/* // @require https://cdn.jsdelivr.net/gh/StylusThemes/Userscripts@c185c2777d00a6826a8bf3c43bbcdcfeba5a9566/libs/utils/utils.min.js // @grant none // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com // @homepageURL https://github.com/StylusThemes/Userscripts // @downloadURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/github-latest.user.js // @updateURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/github-latest.user.js // ==/UserScript== (async function() { 'use strict'; const logger = Logger('GH - Latest', { debug: false }); const BUTTON_ID = 'latest-issues-button'; const QUERY_STRING = 'q=sort%3Aupdated-desc'; // Find a suitable tab to use as a template for cloning (preferably Issues tab) const findTemplateTab = (navBody) => { const issuesAnchor = navBody.querySelector('a[href*="/issues"]'); if (issuesAnchor) { logger.debug('Issues tab found as template'); return issuesAnchor.closest(':scope > *') || issuesAnchor; } // Fallback: pick the first child containing an anchor const fallback = Array.from(navBody.children).find(child => child.querySelector && child.querySelector('a')); if (fallback) logger.debug('Fallback tab found as template'); return fallback || null; }; // Clone a tab template and customize it as "Latest issues" tab const createLatestIssuesTab = (templateTab) => { const clone = templateTab.cloneNode(true); const anchor = clone.querySelector('a') || clone; if (!anchor) { logger.warn('Template tab has no anchor'); return clone; } anchor.id = BUTTON_ID; // Safely set the href to include the "latest issues" query try { const url = new URL(anchor.href, location.origin); anchor.href = `${url.pathname}?${QUERY_STRING}`; } catch { anchor.href = `${(anchor.href || '#').split('?')[0]}?${QUERY_STRING}`; logger.warn('Fallback href applied for Latest issues tab'); } anchor.style.float = 'right'; if (clone.style) clone.style.marginLeft = 'auto'; const svg = clone.querySelector('svg'); if (svg) { svg.setAttribute('viewBox', '0 0 16 16'); svg.style.margin = '0 4px'; svg.innerHTML = ``; } const span = clone.querySelector('span'); if (span) span.textContent = 'Latest issues'; const counter = clone.querySelector('.Counter, .counter'); if (counter) counter.remove(); return clone; }; // Insert the "Latest issues" tab into the nav const addLatestIssuesButton = async () => { const NAV_SELECTOR = 'nav.js-repo-nav > .UnderlineNav-body'; try { const tryAdd = (navBody) => { if (!navBody) return false; // Avoid duplicates if (navBody.querySelector(`#${BUTTON_ID}`)) { logger.debug('Latest issues button already exists'); return true; } const template = findTemplateTab(navBody); if (!template) { logger.warn('No suitable template tab found'); return false; } navBody.appendChild(createLatestIssuesTab(template)); logger('Latest issues button added'); return true; }; // Initial attempt const navBody = document.querySelector(NAV_SELECTOR); if (tryAdd(navBody)) return; // If nav isn't present yet (SPA load), observe DOM until it appears const observer = new MutationObserver((_, obs) => { const navBody = document.querySelector(NAV_SELECTOR); if (tryAdd(navBody)) { obs.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); } catch (err) { logger.error('Failed to add Latest issues button:', err); } }; // Debounced wrapper to prevent excessive DOM updates const debouncedAddButton = debounce(addLatestIssuesButton, 200); // Initial call and SPA navigation handling const init = () => { debouncedAddButton(); document.addEventListener('turbo:render', () => { logger.debug('turbo:render detected, updating Latest issues tab'); debouncedAddButton(); }); }; init(); })();