diff --git a/content.css b/content.css index 2e49755..fd65209 100644 --- a/content.css +++ b/content.css @@ -1,3 +1,19 @@ .coderabbit-hidden { display: none !important; } + +/* ── Toast notification ─────────────────────────────────────────────── */ +#coderabbit-toast { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; +} + +/* ── Scroll-to highlight animation ──────────────────────────────────── */ +@keyframes coderabbit-flash { + 0% { outline-color: rgba(9, 105, 218, 0.8); } + 100% { outline-color: transparent; } +} + +/* ── Dark mode support for injected elements ────────────────────────── */ +[data-coderabbit-theme="dark"] #coderabbit-toast { + filter: invert(1) hue-rotate(180deg); +} diff --git a/content.js b/content.js index 46683b6..2ba34df 100644 --- a/content.js +++ b/content.js @@ -9,6 +9,8 @@ const SEVERITY_PATTERNS = { let currentSettings = { nitpick: true, suggestion: true, warning: true, issue: true }; +// ── Comment detection helpers ────────────────────────────────────────── + function isCodeRabbitComment(commentEl) { const authorEl = commentEl.querySelector(".author") || @@ -18,10 +20,7 @@ function isCodeRabbitComment(commentEl) { } function detectSeverity(commentEl) { - const body = - commentEl.querySelector(".comment-body") || - commentEl.querySelector(".review-comment-body") || - commentEl.querySelector("td.comment-body"); + const body = getCommentBody(commentEl); if (!body) return null; const text = body.textContent; @@ -31,12 +30,158 @@ function detectSeverity(commentEl) { return null; } +function getCommentBody(commentEl) { + return ( + commentEl.querySelector(".comment-body") || + commentEl.querySelector(".review-comment-body") || + commentEl.querySelector("td.comment-body") + ); +} + function getCommentElements() { return document.querySelectorAll( ".timeline-comment, .review-comment, .js-comment, [data-body-version]" ); } +// ── Severity counting ────────────────────────────────────────────────── + +function countSeverities() { + const counts = { nitpick: 0, suggestion: 0, warning: 0, issue: 0 }; + getCommentElements().forEach((el) => { + if (!isCodeRabbitComment(el)) return; + const severity = detectSeverity(el); + if (severity) counts[severity]++; + }); + return counts; +} + +// ── File grouping ────────────────────────────────────────────────────── + +function getFileForComment(commentEl) { + // Try to find the file path from the surrounding diff/file context + const threadContainer = commentEl.closest( + ".js-resolvable-timeline-thread-container" + ); + if (threadContainer) { + const fileLink = threadContainer.querySelector( + ".file-info a, .file-header a, a[title]" + ); + if (fileLink) return fileLink.textContent.trim() || fileLink.getAttribute("title") || ""; + } + + // Try file header above the comment + const fileContainer = commentEl.closest("[data-file-path]"); + if (fileContainer) { + return fileContainer.getAttribute("data-file-path"); + } + + return "(general)"; +} + +function getFileGroups() { + const groups = {}; + + getCommentElements().forEach((el) => { + if (!isCodeRabbitComment(el)) return; + const severity = detectSeverity(el); + if (!severity) return; + + const file = getFileForComment(el); + if (!groups[file]) { + groups[file] = { nitpick: 0, suggestion: 0, warning: 0, issue: 0 }; + } + groups[file][severity]++; + }); + + return Object.entries(groups) + .map(([file, counts]) => ({ + file, + counts, + total: Object.values(counts).reduce((s, v) => s + v, 0), + })) + .sort((a, b) => b.total - a.total); +} + +// ── Scroll to file ───────────────────────────────────────────────────── + +function scrollToFile(fileName) { + // Find the file header element on the page + const fileHeaders = document.querySelectorAll( + "[data-file-path], .file-info a, .file-header a" + ); + + for (const header of fileHeaders) { + const path = + header.getAttribute("data-file-path") || + header.textContent.trim() || + header.getAttribute("title"); + if (path === fileName) { + const target = header.closest(".js-file, .file, .js-diff-progressive-container") || header; + target.scrollIntoView({ behavior: "smooth", block: "start" }); + // Flash highlight + target.style.transition = "outline 0.3s"; + target.style.outline = "3px solid #0969da"; + setTimeout(() => { + target.style.outline = ""; + }, 2000); + return true; + } + } + return false; +} + +// ── Toast notifications ──────────────────────────────────────────────── + +const TOAST_ID = "coderabbit-toast"; + +function showToast(message, severity) { + let toast = document.getElementById(TOAST_ID); + if (!toast) { + toast = document.createElement("div"); + toast.id = TOAST_ID; + toast.setAttribute("role", "status"); + toast.setAttribute("aria-live", "polite"); + document.body.appendChild(toast); + } + + const colors = { + info: { bg: "#ddf4ff", border: "#54aeff", text: "#0550ae" }, + success: { bg: "#dafbe1", border: "#4ac26b", text: "#116329" }, + warning: { bg: "#fff8c5", border: "#d4a72c", text: "#633c01" }, + }; + + const style = colors[severity] || colors.info; + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(0); + z-index: 99999; + padding: 10px 20px; + border-radius: 8px; + border: 1px solid ${style.border}; + background: ${style.bg}; + color: ${style.text}; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + opacity: 1; + transition: opacity 0.3s, transform 0.3s; + pointer-events: none; + `; + + clearTimeout(toast._hideTimeout); + toast._hideTimeout = setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transform = "translateX(-50%) translateY(10px)"; + }, 2500); +} + +// ── Core filter logic ────────────────────────────────────────────────── + function applyFilters() { let hiddenCount = 0; @@ -48,8 +193,11 @@ function applyFilters() { const shouldShow = currentSettings[severity] !== false; - // Walk up to the nearest hideable container (inline comment thread, timeline item, etc.) - const container = el.closest(".js-timeline-item") || el.closest(".js-resolvable-timeline-thread-container") || el; + // Walk up to the nearest hideable container + const container = + el.closest(".js-timeline-item") || + el.closest(".js-resolvable-timeline-thread-container") || + el; if (shouldShow) { container.style.display = ""; @@ -64,20 +212,75 @@ function applyFilters() { return hiddenCount; } -// Listen for settings changes from popup +// ── Message handling ─────────────────────────────────────────────────── + chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message.type === "UPDATE_SEVERITY_FILTER") { - currentSettings = message.settings; - applyFilters(); - sendResponse({ ok: true }); - } else if (message.type === "GET_HIDDEN_COUNT") { - const hiddenCount = applyFilters(); - sendResponse({ hiddenCount }); + switch (message.type) { + case "UPDATE_SEVERITY_FILTER": { + const prev = { ...currentSettings }; + currentSettings = message.settings; + const hiddenCount = applyFilters(); + + // Show toast for filter changes + const changed = Object.keys(currentSettings).filter( + (k) => currentSettings[k] !== prev[k] + ); + if (changed.length > 0) { + const action = currentSettings[changed[0]] ? "Showing" : "Hiding"; + if (changed.length === 4) { + showToast( + currentSettings[changed[0]] ? "All comments visible" : "All comments hidden", + "info" + ); + } else { + showToast(`${action} ${changed.join(", ")} comments`, "info"); + } + } + + sendResponse({ ok: true }); + break; + } + + case "GET_HIDDEN_COUNT": { + const hiddenCount = applyFilters(); + sendResponse({ hiddenCount }); + break; + } + + case "GET_SEVERITY_COUNTS": { + const counts = countSeverities(); + sendResponse({ counts }); + break; + } + + case "GET_FILE_GROUPS": { + const fileGroups = getFileGroups(); + sendResponse({ fileGroups }); + break; + } + + case "SCROLL_TO_FILE": { + const found = scrollToFile(message.file); + sendResponse({ found }); + break; + } + + case "SET_THEME": { + // Store theme preference for content-injected elements + document.documentElement.setAttribute( + "data-coderabbit-theme", + message.theme + ); + sendResponse({ ok: true }); + break; + } } + return true; }); -// Load saved settings on page load +// ── Initialization ───────────────────────────────────────────────────── + chrome.storage.sync.get("severitySettings", (result) => { if (result.severitySettings) { currentSettings = result.severitySettings; diff --git a/manifest.json b/manifest.json index ddd8496..6857b47 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 3, "name": "CodeRabbit Comment Filter", - "version": "1.0.0", - "description": "Hide CodeRabbit review comments on GitHub by severity level", - "permissions": ["storage"], + "version": "1.2.0", + "description": "Filter CodeRabbit review comments by severity with dark mode, accessibility, and file grouping", + "permissions": ["storage", "activeTab"], "host_permissions": ["https://github.com/*"], "content_scripts": [ { diff --git a/popup.html b/popup.html index de2b0e7..371e2d5 100644 --- a/popup.html +++ b/popup.html @@ -1,36 +1,120 @@ - +
-Toggle comment visibility by severity
- -Toggle comment visibility by severity
-