// ==UserScript== // @name GitHub - Latest // @version 1.9.4 // @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@0171b6b6f24caea737beafbc2a8dacd220b729d8/libs/utils/utils.min.js // @grant none // @run-at document-end // @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'; const NAVIGATION_SELECTOR = 'nav[aria-label="Repository"] > ul'; // Find Issues tab first, fallback to any tab with anchor const findTemplateTab = (navigationBody) => { const issuesAnchor = navigationBody.querySelector('a[href*="/issues"]'); if (issuesAnchor) { const listItem = issuesAnchor.closest('li'); if (listItem) return listItem; return issuesAnchor.closest(':scope > *') || issuesAnchor; } // Fallback: any child that contains an anchor const fallback = [...navigationBody.children].find(child => child.querySelector && child.querySelector('a')); if (fallback) logger.debug('Fallback tab found as template'); return fallback || null; }; // Clone template tab and transform it into "Latest issues" with custom icon and query const createLatestIssuesTab = (templateTab) => { const clonedTab = templateTab.cloneNode(true); const anchorElement = clonedTab.querySelector('a') || clonedTab; if (!anchorElement) return clonedTab; anchorElement.id = BUTTON_ID; // Preserve base path while replacing query string for latest issues try { const urlObject = new URL(anchorElement.href, location.origin); anchorElement.href = `${urlObject.pathname}?${QUERY_STRING}`; } catch { anchorElement.href = `${(anchorElement.href || '#').split('?')[0]}?${QUERY_STRING}`; } // Remove aria-current to prevent underline styling for latest issues tab anchorElement.removeAttribute('aria-current'); anchorElement.style.float = 'none'; if (clonedTab.style) clonedTab.style.marginLeft = 'auto'; // Replace existing icon with flame SVG for "latest" indicator const svgElement = clonedTab.querySelector('svg'); if (svgElement) { svgElement.setAttribute('viewBox', '0 0 16 16'); svgElement.innerHTML = ``; } const textSpan = clonedTab.querySelector('[data-component="text"]'); if (textSpan) { textSpan.textContent = 'Latest issues'; textSpan.setAttribute('data-content', 'Latest issues'); } else { const simpleSpan = clonedTab.querySelector('span'); if (simpleSpan) simpleSpan.textContent = 'Latest issues'; } const counterElement = clonedTab.querySelector('[data-component="counter"], .Counter, .counter'); if (counterElement) counterElement.remove(); return clonedTab; }; const addLatestIssuesButton = () => { // If the button already exists in the DOM, do nothing if (document.getElementById(BUTTON_ID)) return; const navigationBody = document.querySelector(NAVIGATION_SELECTOR); if (!navigationBody) { logger.debug('Navigation selector not found'); return; } const templateTab = findTemplateTab(navigationBody); if (!templateTab) { logger.debug('Template tab not found'); return; } logger.debug('Adding latest issues button'); navigationBody.appendChild(createLatestIssuesTab(templateTab)); }; const initialize = () => { logger('Initializing GitHub Latest Issues script'); // 1. Initial attempt addLatestIssuesButton(); // 2. Persistent Observer: This watches for GitHub wiping the nav bar // We do NOT disconnect this observer. It stays alive to fight React re-renders. const observer = new MutationObserver(() => { addLatestIssuesButton(); }); observer.observe(document.body, { childList: true, subtree: true }); // 3. Handle specific Turbo events (SPA navigation) document.addEventListener('turbo:render', () => { logger.debug('Turbo render event, re-adding latest issues button'); addLatestIssuesButton(); }); }; initialize(); })();