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 @@ - + -

CodeRabbit Filter

-

Toggle comment visibility by severity

- -
- - nitpick - - -
+
-
- - suggestion - - +
+

CodeRabbit Filter

+
+

Toggle comment visibility by severity

-
- - warning - - + +
+
+ + nitpick + + + 0 + + +
+ +
+ + suggestion + + + 0 + + +
+ +
+ + warning + + + 0 + + +
+ +
+ + issue + + + 0 + + +
-
- - issue - - +
+ + +
+ + +
-
+ +
+
Comments by File
+
+
Navigate to a GitHub PR to see file breakdown
+
+
diff --git a/popup.js b/popup.js index 27670d4..db54099 100644 --- a/popup.js +++ b/popup.js @@ -1,6 +1,13 @@ const SEVERITIES = ["nitpick", "suggestion", "warning", "issue"]; const DEFAULT_SETTINGS = { nitpick: true, suggestion: true, warning: true, issue: true }; +const BADGE_COLORS = { + nitpick: "#818b98", + suggestion: "#0969da", + warning: "#bf8700", + issue: "#cf222e", +}; + async function loadSettings() { const result = await chrome.storage.sync.get("severitySettings"); return result.severitySettings || { ...DEFAULT_SETTINGS }; @@ -10,31 +17,173 @@ async function saveSettings(settings) { await chrome.storage.sync.set({ severitySettings: settings }); } -async function notifyContentScript(settings) { +async function getActiveTab() { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + return tab; +} + +async function notifyContentScript(settings) { + const tab = await getActiveTab(); if (tab?.id) { chrome.tabs.sendMessage(tab.id, { type: "UPDATE_SEVERITY_FILTER", settings }); } } +function announce(message) { + const region = document.getElementById("liveRegion"); + region.textContent = message; +} + async function updateHiddenCount() { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + const tab = await getActiveTab(); if (!tab?.id) return; try { const response = await chrome.tabs.sendMessage(tab.id, { type: "GET_HIDDEN_COUNT" }); const el = document.getElementById("hiddenCount"); if (response && response.hiddenCount > 0) { - el.textContent = `${response.hiddenCount} comment${response.hiddenCount === 1 ? "" : "s"} hidden`; + const msg = `${response.hiddenCount} comment${response.hiddenCount === 1 ? "" : "s"} hidden`; + el.textContent = msg; + announce(msg); } else { el.textContent = "All comments visible"; + announce("All comments visible"); } } catch { document.getElementById("hiddenCount").textContent = "Navigate to a GitHub PR to use"; } } +async function updateSeverityCounts() { + const tab = await getActiveTab(); + if (!tab?.id) return; + + try { + const response = await chrome.tabs.sendMessage(tab.id, { type: "GET_SEVERITY_COUNTS" }); + if (response?.counts) { + for (const severity of SEVERITIES) { + const el = document.getElementById(`count-${severity}`); + if (el) el.textContent = response.counts[severity] || 0; + } + } + } catch { + // Page not ready + } +} + +async function updateFileGroups() { + const tab = await getActiveTab(); + if (!tab?.id) return; + + const container = document.getElementById("fileGroupList"); + + try { + const response = await chrome.tabs.sendMessage(tab.id, { type: "GET_FILE_GROUPS" }); + if (!response?.fileGroups || response.fileGroups.length === 0) { + container.innerHTML = '
No CodeRabbit comments found
'; + return; + } + + container.innerHTML = response.fileGroups + .map((fg) => { + const badges = Object.entries(fg.counts) + .filter(([, count]) => count > 0) + .map( + ([severity, count]) => + `${count}` + ) + .join(""); + + return ` +
+ ${escapeHtml(fg.file)} + ${badges} +
`; + }) + .join(""); + + // Click to scroll to file on the page + container.querySelectorAll(".file-group-item").forEach((item) => { + const handler = () => { + const file = item.dataset.file; + chrome.tabs.sendMessage(tab.id, { type: "SCROLL_TO_FILE", file }); + }; + item.addEventListener("click", handler); + item.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handler(); + } + }); + }); + } catch { + container.innerHTML = '
Navigate to a GitHub PR to see file breakdown
'; + } +} + +function escapeHtml(str) { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +function escapeAttr(str) { + return str.replace(/&/g, "&").replace(/"/g, """).replace(/ { + // Theme + let currentTheme = await loadTheme(); + applyTheme(currentTheme); + + document.getElementById("themeToggle").addEventListener("click", async () => { + // Cycle: system -> dark -> light -> system + const order = ["system", "dark", "light"]; + const idx = order.indexOf(currentTheme); + currentTheme = order[(idx + 1) % order.length]; + await saveTheme(currentTheme); + applyTheme(currentTheme); + // Also tell content script about theme + const tab = await getActiveTab(); + if (tab?.id) { + chrome.tabs.sendMessage(tab.id, { type: "SET_THEME", theme: currentTheme }); + } + }); + + // Severity toggles const settings = await loadSettings(); document.querySelectorAll("[data-severity]").forEach((checkbox) => { @@ -45,9 +194,42 @@ document.addEventListener("DOMContentLoaded", async () => { settings[severity] = checkbox.checked; await saveSettings(settings); await notifyContentScript(settings); - setTimeout(updateHiddenCount, 100); + setTimeout(() => { + updateHiddenCount(); + updateSeverityCounts(); + updateFileGroups(); + }, 100); + }); + }); + + // Quick actions + document.getElementById("showAll").addEventListener("click", async () => { + for (const s of SEVERITIES) settings[s] = true; + document.querySelectorAll("[data-severity]").forEach((cb) => (cb.checked = true)); + await saveSettings(settings); + await notifyContentScript(settings); + setTimeout(() => { updateHiddenCount(); updateSeverityCounts(); updateFileGroups(); }, 100); + }); + + document.getElementById("hideAll").addEventListener("click", async () => { + for (const s of SEVERITIES) settings[s] = false; + document.querySelectorAll("[data-severity]").forEach((cb) => (cb.checked = false)); + await saveSettings(settings); + await notifyContentScript(settings); + setTimeout(() => { updateHiddenCount(); updateSeverityCounts(); updateFileGroups(); }, 100); + }); + + document.getElementById("issuesOnly").addEventListener("click", async () => { + for (const s of SEVERITIES) settings[s] = s === "issue"; + document.querySelectorAll("[data-severity]").forEach((cb) => { + cb.checked = cb.dataset.severity === "issue"; }); + await saveSettings(settings); + await notifyContentScript(settings); + setTimeout(() => { updateHiddenCount(); updateSeverityCounts(); updateFileGroups(); }, 100); }); updateHiddenCount(); + updateSeverityCounts(); + updateFileGroups(); });