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
47 changes: 43 additions & 4 deletions web/src/components/DiffView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
lazy,
Suspense,
type CSSProperties,
type PointerEvent as ReactPointerEvent,
} from "react";
import { useParams, useSearchParams, Link } from "react-router";
import {
Expand Down Expand Up @@ -92,6 +93,9 @@ import { DEFAULT_CODE_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, prependFontFamily } f

const MobileSidebarDrawer = lazy(() => import("./diff-view/MobileSidebarDrawer"));

const SIDEBAR_WIDTH_KEY = "diffs-sidebar-width";
const DEFAULT_SIDEBAR_WIDTH = 320;

const codeViewStyle = {
flex: 1,
overflow: "auto" as const,
Expand Down Expand Up @@ -305,6 +309,26 @@ export function DiffView({ source = "pr" }: { source?: "pr" | "local" | "branch"
const [shortcutsOpen, setShortcutsOpen] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState(
() => Number(localStorage.getItem(SIDEBAR_WIDTH_KEY)) || DEFAULT_SIDEBAR_WIDTH,
);
useEffect(() => {
localStorage.setItem(SIDEBAR_WIDTH_KEY, String(sidebarWidth));
}, [sidebarWidth]);
const startSidebarResize = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
event.preventDefault();
const handle = event.currentTarget;
const left = handle.parentElement!.getBoundingClientRect().left;
handle.setPointerCapture(event.pointerId);
const onMove = (e: PointerEvent) =>
setSidebarWidth(Math.round(Math.min(Math.max(e.clientX - left, 200), 640)));
const onUp = () => {
handle.removeEventListener("pointermove", onMove);
handle.removeEventListener("pointerup", onUp);
};
handle.addEventListener("pointermove", onMove);
handle.addEventListener("pointerup", onUp);
}, []);
const [submittingPendingComments, setSubmittingPendingComments] = useState(false);
const [exporting, setExporting] = useState(false);
const [repoContext, setRepoContext] = useState<{
Expand Down Expand Up @@ -555,15 +579,18 @@ export function DiffView({ source = "pr" }: { source?: "pr" | "local" | "branch"
if (reviewed.key !== reviewedStorageKey) {
setReviewed({ key: reviewedStorageKey, map: readReviewedSignatures(reviewedStorageKey) });
}
const hiddenReviewedNames = useMemo(() => {
const reviewedNames = useMemo(() => {
const names = new Set<string>();
if (!hideReviewed) return names;
for (const f of files) {
const sig = fileSignatures.get(f.name);
if (sig != null && reviewed.map.get(f.name) === sig) names.add(f.name);
}
return names;
}, [hideReviewed, files, fileSignatures, reviewed]);
}, [files, fileSignatures, reviewed]);
const hiddenReviewedNames = useMemo(
() => (hideReviewed ? reviewedNames : new Set<string>()),
[hideReviewed, reviewedNames],
);
const visibleFiles = useMemo(
() =>
hiddenReviewedNames.size === 0
Expand Down Expand Up @@ -1403,6 +1430,7 @@ export function DiffView({ source = "pr" }: { source?: "pr" | "local" | "branch"
const sidebarTreeProps = {
paths: filePaths,
files: visibleFiles,
reviewedPaths: reviewedNames,
comments: commentThreads,
onFileActivate: scrollToFile,
onCommentActivate: scrollToThread,
Expand Down Expand Up @@ -1442,8 +1470,19 @@ export function DiffView({ source = "pr" }: { source?: "pr" | "local" | "branch"

<div className="flex min-h-0 flex-1">
{sidebarOpen && (
<aside className="hidden w-[320px] shrink-0 overflow-hidden border-r border-neutral-200 bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 md:block">
<aside
className="relative hidden shrink-0 overflow-hidden border-r border-neutral-200 bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 md:block"
style={{ width: sidebarWidth }}
>
<SidebarTree {...sidebarTreeProps} />
<div
role="separator"
aria-orientation="vertical"
aria-label="Resize sidebar"
className="absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize hover:bg-neutral-300 dark:hover:bg-neutral-600"
onPointerDown={startSidebarResize}
onDoubleClick={() => setSidebarWidth(DEFAULT_SIDEBAR_WIDTH)}
/>
</aside>
)}
{mobileSidebarOpen && (
Expand Down
32 changes: 32 additions & 0 deletions web/src/components/diff-view/SidebarTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ function gitStatusForFile(file: FileDiffMetadata): GitStatusEntry["status"] {
}
}

// "a/b/c.ts" -> ["a/", "a/b/"], matching the tree's folder row paths.
function ancestorDirs(path: string): string[] {
const parts = path.split("/").slice(0, -1);
return parts.map((_, i) => `${parts.slice(0, i + 1).join("/")}/`);
}

function SidebarIconButton({
tooltip,
...buttonProps
Expand All @@ -64,6 +70,7 @@ function SidebarIconButton({
export function SidebarTree({
paths,
files,
reviewedPaths,
comments,
onFileActivate,
onCommentActivate,
Expand All @@ -73,6 +80,7 @@ export function SidebarTree({
}: {
paths: readonly string[];
files: readonly FileDiffMetadata[];
reviewedPaths: ReadonlySet<string>;
comments: readonly ReviewThread[];
onFileActivate: (path: string) => void;
onCommentActivate: (thread: ReviewThread) => void;
Expand Down Expand Up @@ -144,6 +152,30 @@ export function SidebarTree({
useEffect(() => {
model.setGitStatus(gitStatus);
}, [model, gitStatus]);
// The tree has no per-row styling API (and unsafeCSS is fixed at creation), so dim
// reviewed rows via a style element in its open shadow root. The shadow root is attached
// after this effect on (re)mount, hence the frame delay. Folders (rows keyed "dir/") dim
// once every file under them is reviewed.
useEffect(() => {
const unreviewedDirs = new Set(
paths.filter((path) => !reviewedPaths.has(path)).flatMap(ancestorDirs),
);
const dimmed = [...reviewedPaths].flatMap((path) => [
path,
...ancestorDirs(path).filter((dir) => !unreviewedDirs.has(dir)),
]);
const style = document.createElement("style");
style.textContent = [...new Set(dimmed)]
.map((path) => `[data-item-path="${CSS.escape(path)}"] { opacity: 0.5; }`)
.join("\n");
const frame = requestAnimationFrame(() =>
model.getFileTreeContainer()?.shadowRoot?.append(style),
);
return () => {
cancelAnimationFrame(frame);
style.remove();
};
}, [model, paths, reviewedPaths, section]);

const search = useFileTreeSearch(model);
const fileTreeStyle = useMemo<FileTreeStyle>(
Expand Down
Loading