Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { injectCommitDiffStats } from "./features/commit-diff-stats";
import { injectBetterTopRepos } from "./features/better-top-repos";
import { injectWatchForkStarPopup } from "./features/watch-fork-star-popup";
import { injectPRCollapseExpand } from "./features/pr-collapse-expand";
import { reserveInfoRowSkeletons } from "./lib/info-row-skeleton";
import { applyPageMarker } from "./lib/page-marker";

const FEATURE_KEYS = [
"feature-pr-branch-names",
Expand All @@ -32,15 +34,15 @@ type FeatureKey = (typeof FEATURE_KEYS)[number];

// CSS classes used by each feature's injected elements
const FEATURE_CLASSES: Record<FeatureKey, string[]> = {
"feature-pr-branch-names": ["better-github-branch-badge"],
"feature-pr-branch-names": ["better-github-branch-badge", "bg-skeleton-pill--branch"],
"feature-pr-review-status": ["better-github-review-status"],
"feature-pr-diff-stats": ["better-github-diff-stats"],
"feature-pr-diff-stats": ["better-github-diff-stats", "bg-skeleton-pill--pr-diff"],
"feature-release-tab": ["better-github-releases-tab"],
"feature-pr-label-position": ["better-github-label-prefix"],
"feature-pr-approve-now": ["better-github-approve-now", "better-github-approve-dialog-overlay"],
"feature-default-sort": [],
"feature-commit-tags": ["better-github-commit-tag"],
"feature-commit-diff-stats": ["better-github-commit-diff-stats"],
"feature-commit-diff-stats": ["better-github-commit-diff-stats", "bg-skeleton-pill--commit-diff"],
"feature-better-top-repos": [],
"feature-watch-fork-star-popup": ["bg-wfs-counter-wrap"],
"feature-pr-collapse-expand": ["better-github-toggle-tree", "better-github-collapse-expand"],
Expand Down Expand Up @@ -140,11 +142,20 @@ if (isExtensionValid()) {

// On each navigation, inject enabled features
onPageReady(async () => {
// Keep <html data-bg-page> in sync with the current URL so
// skeleton-reserve.css matches the right row selector after SPA navs.
applyPageMarker();

// Always-on features
injectFileAgeColor();

// Toggleable features
const flags = await getFeatureFlags();

// Reserve row height with skeleton placeholders before any async fetch starts
// — avoids layout-shift "flash" when real badges arrive.
reserveInfoRowSkeletons(flags);

for (const key of FEATURE_KEYS) {
if (flags[key]) {
injectFeature(key);
Expand Down
5 changes: 5 additions & 0 deletions src/early-sort-redirect.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// Runs at document_start — before page renders, so no visible flicker.
// Handles early redirects that must happen before the page renders.
// Respects the feature toggles in storage.
import { applyPageMarker } from "./lib/page-marker";

(function () {
const path = location.pathname;

// Must run sync before body parse so skeleton-reserve.css matches.
applyPageMarker();

// --- Default sort for PR/issue lists ---
if (!/^\/[^/]+\/[^/]+\/(pulls|issues)\/?$/.test(path)) return;

Expand Down
25 changes: 15 additions & 10 deletions src/features/commit-diff-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isCommitsListPage, getRepoInfo } from "../lib/page-detect";
import { fetchCommitDiffStats } from "../lib/github-api";
import { collectCommitRows, MAIN_CONTENT_INNER_SELECTOR } from "../lib/commit-dom";
import { buildDiffStatsBadge } from "../lib/diff-stats-badge";
import { clearSkeletons } from "../lib/info-row-skeleton";

const BADGE_CLASS = "better-github-commit-diff-stats";

Expand All @@ -14,18 +15,22 @@ export async function injectCommitDiffStats(): Promise<void> {
const shaToContainer = collectCommitRows(info.owner, info.repo);
if (shaToContainer.size === 0) return;

const stats = await fetchCommitDiffStats(info.owner, info.repo, [...shaToContainer.keys()]);
if (stats.length === 0) return;
try {
const stats = await fetchCommitDiffStats(info.owner, info.repo, [...shaToContainer.keys()]);
if (stats.length === 0) return;

const statsMap = new Map(stats.map((s) => [s.sha, s]));
const statsMap = new Map(stats.map((s) => [s.sha, s]));

for (const [sha, container] of shaToContainer) {
const stat = statsMap.get(sha);
if (!stat) continue;
if (container.querySelector(`.${BADGE_CLASS}`)) continue;
for (const [sha, container] of shaToContainer) {
const stat = statsMap.get(sha);
if (!stat) continue;
if (container.querySelector(`.${BADGE_CLASS}`)) continue;

const mainInner = container.querySelector<HTMLElement>(MAIN_CONTENT_INNER_SELECTOR);
const parent = mainInner || container;
parent.appendChild(buildDiffStatsBadge(stat, BADGE_CLASS));
const mainInner = container.querySelector<HTMLElement>(MAIN_CONTENT_INNER_SELECTOR);
const parent = mainInner || container;
parent.appendChild(buildDiffStatsBadge(stat, BADGE_CLASS));
}
} finally {
clearSkeletons("commitDiff");
}
}
48 changes: 25 additions & 23 deletions src/features/pr-branch-names.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isPRListPage, getRepoInfo, getPRListParams } from "../lib/page-detect";
import { fetchPRBranches } from "../lib/github-api";
import { getOrCreateInfoRow } from "../lib/info-row";
import { clearSkeletons } from "../lib/info-row-skeleton";

const BADGE_CLASS = "better-github-branch-badge";
const COPIED_CLASS = "better-github-branch-copied";
Expand Down Expand Up @@ -48,36 +49,37 @@ export async function injectPRBranchNames(): Promise<void> {
const existing = document.querySelectorAll(`.${BADGE_CLASS}`);
if (existing.length > 0) return;

const { state, page } = getPRListParams();
const branches = await fetchPRBranches(info.owner, info.repo, state, page);
try {
const { state, page } = getPRListParams();
const branches = await fetchPRBranches(info.owner, info.repo, state, page);

if (branches.length === 0) return;
if (branches.length === 0) return;

const branchMap = new Map(branches.map((b) => [b.number, b.headRef]));
const branchMap = new Map(branches.map((b) => [b.number, b.headRef]));
const prRows = document.querySelectorAll("[id^='issue_']");

// Find PR rows — GitHub uses [id^='issue_'] for PR list items
const prRows = document.querySelectorAll("[id^='issue_']");
for (const row of prRows) {
const id = row.getAttribute("id");
if (!id) continue;

for (const row of prRows) {
const id = row.getAttribute("id");
if (!id) continue;
const prNumber = parseInt(id.replace("issue_", ""), 10);
const branchName = branchMap.get(prNumber);
if (!branchName) continue;

const prNumber = parseInt(id.replace("issue_", ""), 10);
const branchName = branchMap.get(prNumber);
if (!branchName) continue;
if (row.querySelector(`.${BADGE_CLASS}`)) continue;

// Don't inject if already present
if (row.querySelector(`.${BADGE_CLASS}`)) continue;
const infoRow = getOrCreateInfoRow(row);
if (!infoRow) continue;

const infoRow = getOrCreateInfoRow(row);
if (!infoRow) continue;
const badge = document.createElement("span");
badge.className = BADGE_CLASS;
badge.textContent = branchName;
badge.dataset.branch = branchName;
badge.title = "Click to copy branch name";

const badge = document.createElement("span");
badge.className = BADGE_CLASS;
badge.textContent = branchName;
badge.dataset.branch = branchName;
badge.title = "Click to copy branch name";

infoRow.appendChild(badge);
infoRow.appendChild(badge);
}
} finally {
clearSkeletons("branch");
}
}
31 changes: 18 additions & 13 deletions src/features/pr-diff-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isPRListPage, getRepoInfo } from "../lib/page-detect";
import { fetchPRDiffStats } from "../lib/github-api";
import { getOrCreateInfoRow } from "../lib/info-row";
import { buildDiffStatsBadge } from "../lib/diff-stats-badge";
import { clearSkeletons } from "../lib/info-row-skeleton";

const BADGE_CLASS = "better-github-diff-stats";

Expand All @@ -21,24 +22,28 @@ export async function injectPRDiffStats(): Promise<void> {

if (prNumbers.length === 0) return;

const stats = await fetchPRDiffStats(info.owner, info.repo, prNumbers);
if (stats.length === 0) return;
try {
const stats = await fetchPRDiffStats(info.owner, info.repo, prNumbers);
if (stats.length === 0) return;

const statsMap = new Map(stats.map((s) => [s.number, s]));
const statsMap = new Map(stats.map((s) => [s.number, s]));

for (const row of prRows) {
const id = row.getAttribute("id");
if (!id) continue;
for (const row of prRows) {
const id = row.getAttribute("id");
if (!id) continue;

const prNumber = parseInt(id.replace("issue_", ""), 10);
const stat = statsMap.get(prNumber);
if (!stat) continue;
const prNumber = parseInt(id.replace("issue_", ""), 10);
const stat = statsMap.get(prNumber);
if (!stat) continue;

if (row.querySelector(`.${BADGE_CLASS}`)) continue;
if (row.querySelector(`.${BADGE_CLASS}`)) continue;

const infoRow = getOrCreateInfoRow(row);
if (!infoRow) continue;
const infoRow = getOrCreateInfoRow(row);
if (!infoRow) continue;

infoRow.appendChild(buildDiffStatsBadge(stat, BADGE_CLASS));
infoRow.appendChild(buildDiffStatsBadge(stat, BADGE_CLASS));
}
} finally {
clearSkeletons("prDiff");
}
}
89 changes: 89 additions & 0 deletions src/lib/info-row-skeleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { isPRListPage, isCommitsListPage, getRepoInfo } from "./page-detect";
import { collectCommitRows, MAIN_CONTENT_INNER_SELECTOR } from "./commit-dom";
import { getOrCreateInfoRow } from "./info-row";

export type SkeletonKind = "branch" | "prDiff" | "commitDiff";

const SKELETONS: Record<SkeletonKind, { skeleton: string; real: string }> = {
branch: { skeleton: "bg-skeleton-pill--branch", real: "better-github-branch-badge" },
prDiff: { skeleton: "bg-skeleton-pill--pr-diff", real: "better-github-diff-stats" },
commitDiff: {
skeleton: "bg-skeleton-pill--commit-diff",
real: "better-github-commit-diff-stats",
},
};

const SKELETON_BASE_CLASS = "bg-skeleton-pill";

export interface SkeletonFlags {
"feature-pr-branch-names"?: boolean;
"feature-pr-diff-stats"?: boolean;
"feature-commit-diff-stats"?: boolean;
}

function buildPill(extraClass: string): HTMLSpanElement {
const span = document.createElement("span");
span.className = `${SKELETON_BASE_CLASS} ${extraClass}`;
span.setAttribute("aria-hidden", "true");
return span;
}

function hasChild(scope: Element, cls: string): boolean {
return scope.querySelector(`.${cls}`) !== null;
}

export function reserveInfoRowSkeletons(flags: SkeletonFlags): void {
if (isPRListPage()) {
reservePRListSkeletons(flags);
return;
}
if (isCommitsListPage()) {
reserveCommitsListSkeletons(flags);
}
}

function reservePRListSkeletons(flags: SkeletonFlags): void {
const wantBranch = !!flags["feature-pr-branch-names"];
const wantDiff = !!flags["feature-pr-diff-stats"];
if (!wantBranch && !wantDiff) return;

const branch = SKELETONS.branch;
const prDiff = SKELETONS.prDiff;
const probeSelector = [branch.real, branch.skeleton, prDiff.real, prDiff.skeleton]
.map((c) => `.${c}`)
.join(", ");

for (const row of document.querySelectorAll("[id^='issue_']")) {
const present = new Set(
[...row.querySelectorAll(probeSelector)].flatMap((el) => [...el.classList]),
);
const needBranch = wantBranch && !present.has(branch.real) && !present.has(branch.skeleton);
const needDiff = wantDiff && !present.has(prDiff.real) && !present.has(prDiff.skeleton);
if (!needBranch && !needDiff) continue;

const infoRow = getOrCreateInfoRow(row);
if (!infoRow) continue;

if (needBranch) infoRow.appendChild(buildPill(branch.skeleton));
if (needDiff) infoRow.appendChild(buildPill(prDiff.skeleton));
}
}

function reserveCommitsListSkeletons(flags: SkeletonFlags): void {
if (!flags["feature-commit-diff-stats"]) return;

const info = getRepoInfo();
if (!info) return;

const { skeleton, real } = SKELETONS.commitDiff;
for (const container of collectCommitRows(info.owner, info.repo).values()) {
if (hasChild(container, real) || hasChild(container, skeleton)) continue;

const mainInner = container.querySelector<HTMLElement>(MAIN_CONTENT_INNER_SELECTOR);
(mainInner || container).appendChild(buildPill(skeleton));
}
}

export function clearSkeletons(kind: SkeletonKind): void {
document.querySelectorAll(`.${SKELETONS[kind].skeleton}`).forEach((el) => el.remove());
}
29 changes: 29 additions & 0 deletions src/lib/page-marker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Page-type marker on `<html data-bg-page>`. Read by `skeleton-reserve.css`
* (loaded at document_start) to pre-reserve info-row height before GitHub
* paints — so the row that content.js mounts at document_idle no longer
* pushes everything down.
*
* Must stay sync-friendly: called from early-sort-redirect.ts at
* document_start, and from content.ts on every SPA navigation.
*/

export type PageMarker = "pr-list" | "commits-list";

export function detectPageMarker(path: string): PageMarker | null {
// Mirror isPRListPage / isCommitsListPage in page-detect.ts so the CSS
// reservation only kicks in where the corresponding feature actually runs.
if (/^\/[^/]+\/[^/]+\/pulls\/?$/.test(path)) return "pr-list";
if (/^\/[^/]+\/[^/]+\/commits(\/|$)/.test(path)) return "commits-list";
return null;
}

export function applyPageMarker(): void {
const marker = detectPageMarker(location.pathname);
const root = document.documentElement;
if (marker) {
if (root.dataset.bgPage !== marker) root.dataset.bgPage = marker;
} else if (root.dataset.bgPage) {
delete root.dataset.bgPage;
}
}
Loading