// ==UserScript== // @name GitHub Commit Email Revealer // @namespace https://github.com/ // @version 5.0.1 // @description Shows all commit author emails in a popup next to Browse Files, with live status indicator // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com // @author cee // @match https://github.com/*/*/commit/* // @grant GM_xmlhttpRequest // @connect github.com // @run-at document-idle // @downloadURL https://raw.githubusercontent.com/ceeprus/userscript/main/github/user-mail.user.js // @updateURL https://raw.githubusercontent.com/ceeprus/userscript/main/github/user-mail.user.js // ==/UserScript== (function () { 'use strict'; if (!location.pathname.match(/^\/[^/]+\/[^/]+\/commit\/[0-9a-f]+\/?$/i)) return; const patchUrl = location.href.replace(/\/?(\?.*)?$/, '.patch'); // ── Status states ───────────────────────────────────────────────────────── // Each state: { icon, label, color, bg, border, spin } const STATES = { waiting: { icon: '⋯', label: 'waiting…', color: '#8b949e', bg: 'rgba(139,148,158,.1)', border: 'rgba(139,148,158,.25)', spin: false, }, fetching: { icon: '↻', label: 'fetching .patch…', color: '#d29922', bg: 'rgba(210,153,34,.1)', border: 'rgba(210,153,34,.3)', spin: true, }, parsing: { icon: '↻', label: 'parsing…', color: '#d29922', bg: 'rgba(210,153,34,.1)', border: 'rgba(210,153,34,.3)', spin: true, }, ready: { icon: '✉', label: null, // label set dynamically color: '#58a6ff', bg: 'rgba(88,166,255,.1)', border: 'rgba(88,166,255,.3)', spin: false, }, noreply: { icon: '✉', label: null, color: '#768390', bg: 'rgba(118,131,144,.12)', border: 'rgba(118,131,144,.3)', spin: false, }, error: { icon: '✕', label: 'failed', color: '#f85149', bg: 'rgba(248,81,73,.1)', border: 'rgba(248,81,73,.3)', spin: false, }, }; // ── Pill element ────────────────────────────────────────────────────────── let pillEl = null; let iconEl = null; let labelEl = null; let spinFrame = null; let popup = null; let isOpen = false; let currentEntries = []; function createPill() { pillEl = document.createElement('button'); pillEl.type = 'button'; pillEl.dataset.emailPill = '1'; pillEl.style.cssText = ` display: inline-flex; align-items: center; gap: 5px; padding: 0 10px; height: 28px; border-radius: 6px; font: 12px/1 ui-monospace, 'Cascadia Code', monospace; cursor: default; white-space: nowrap; vertical-align: middle; transition: background .15s, color .15s, border-color .15s; `; iconEl = document.createElement('span'); iconEl.style.cssText = 'opacity:.8; font-style:normal; display:inline-block; transition:transform .1s;'; labelEl = document.createElement('span'); pillEl.appendChild(iconEl); pillEl.appendChild(labelEl); pillEl.addEventListener('click', (e) => { e.stopPropagation(); if (!currentEntries.length) return; if (isOpen) { closePopup(); return; } openPopup(currentEntries, pillEl); }); return pillEl; } function applyState(stateKey, labelOverride) { const s = STATES[stateKey]; pillEl.style.color = s.color; pillEl.style.background = s.bg; pillEl.style.border = `1px solid ${s.border}`; pillEl.style.cursor = (stateKey === 'ready' || stateKey === 'noreply') ? 'pointer' : 'default'; pillEl.title = stateKey === 'fetching' ? patchUrl : stateKey === 'error' ? 'Could not load .patch file' : stateKey === 'ready' ? 'Click to show email(s)' : ''; iconEl.textContent = s.icon; // Spin animation for loading states if (spinFrame) cancelAnimationFrame(spinFrame); if (s.spin) { let angle = 0; const spin = () => { angle = (angle + 4) % 360; iconEl.style.transform = `rotate(${angle}deg)`; spinFrame = requestAnimationFrame(spin); }; spinFrame = requestAnimationFrame(spin); } else { iconEl.style.transform = ''; } labelEl.textContent = labelOverride ?? s.label ?? ''; // Hover pillEl.onmouseenter = currentEntries.length ? () => pillEl.style.background = stateKey === 'noreply' ? 'rgba(118,131,144,.2)' : 'rgba(88,166,255,.18)' : null; pillEl.onmouseleave = () => pillEl.style.background = s.bg; } // ── Popup ───────────────────────────────────────────────────────────────── function closePopup() { if (popup) { popup.remove(); popup = null; } isOpen = false; } function openPopup(entries, anchor) { closePopup(); isOpen = true; popup = document.createElement('div'); popup.style.cssText = ` position: absolute; z-index: 999999; min-width: 280px; max-width: 420px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,.6); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; overflow: hidden; `; // Header const header = document.createElement('div'); header.style.cssText = ` padding: 10px 14px 8px; border-bottom: 1px solid #21262d; display: flex; justify-content: space-between; align-items: center; `; header.innerHTML = ` ${entries.length} Email${entries.length > 1 ? 's' : ''} found view .patch ↗ `; popup.appendChild(header); // Rows entries.forEach(({ name, email, role }, i) => { const isNoreply = email.includes('noreply.github.com'); const row = document.createElement('div'); row.style.cssText = ` padding: 10px 14px; ${i < entries.length - 1 ? 'border-bottom: 1px solid #21262d;' : ''} cursor: ${isNoreply ? 'default' : 'pointer'}; transition: background .1s; `; row.innerHTML = `