diff --git a/src/components/repo.tsx b/src/components/repo.tsx index 7b76047..87650a8 100644 --- a/src/components/repo.tsx +++ b/src/components/repo.tsx @@ -5,6 +5,7 @@ import { Check, ChevronRight, Circle, + CircleCheck, Columns2, Copy, Download, @@ -27,7 +28,7 @@ import { } from 'lucide-solid'; import { marked } from 'marked'; import { A } from '@solidjs/router'; -import { For, Show, createMemo, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; +import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; import type { RepoContext, TreeEntry } from '../lib/api/repos'; import type { Did } from '@atcute/lexicons/syntax'; import { createQuery } from '@tanstack/solid-query'; @@ -1434,13 +1435,21 @@ const scrollToDiffFile = (path: string, event: MouseEvent) => { } }; -const DiffFileTreeNodes: Component<{ nodes: DiffFileTreeNode[] }> = (props) => ( +const DiffFileTreeNodes: Component<{ + nodes: DiffFileTreeNode[]; + reviewedFiles?: () => Set; +}> = (props) => (
- {(node) => } + + {(node) => } +
); -const DiffFileTreeNodeView: Component<{ node: DiffFileTreeNode }> = (props) => ( +const DiffFileTreeNodeView: Component<{ + node: DiffFileTreeNode; + reviewedFiles?: () => Set; +}> = (props) => (
= (props) => (
- +
} > - {(file) => ( - scrollToDiffFile(file().path, event)} - > - - {props.node.name} - - )} + {(file) => { + const isReviewed = () => props.reviewedFiles?.().has(file().path) ?? false; + return ( + scrollToDiffFile(file().path, event)} + > + + {props.node.name} + + + ✓ + + + + ); + }} ); @@ -1484,7 +1501,12 @@ const DiffStatPill: Component<{ additions: number; deletions: number }> = (props ); -export const PullDiffView: Component<{ patch: string; roundLabel: string; history: JSX.Element }> = (props) => { +export const PullDiffView: Component<{ + patch: string; + roundLabel: string; + storageKey: string; + history: JSX.Element; +}> = (props) => { const files = createMemo(() => parseDiffFiles(props.patch)); const stats = createMemo(() => totalDiffStats(files())); const tree = createMemo(() => diffFileTree(files())); @@ -1492,6 +1514,95 @@ export const PullDiffView: Component<{ patch: string; roundLabel: string; histor const [filesVisible, setFilesVisible] = createSignal(true); const [historyVisible, setHistoryVisible] = createSignal(true); + const loadReviewed = (): Set => { + try { + const data = localStorage.getItem(props.storageKey); + if (!data) return new Set(); + const entry = JSON.parse(data); + return new Set(Array.isArray(entry) ? entry : (entry.files || [])); + } catch { + return new Set(); + } + }; + + const [reviewed, setReviewed] = createSignal>(new Set()); + + createEffect(() => { + setReviewed(loadReviewed()); + }); + + const saveReviewed = (newSet: Set) => { + const currentFilePaths = new Set(files().map((f) => f.path)); + const filesToSave = Array.from(newSet).filter((path) => currentFilePaths.has(path)); + try { + localStorage.setItem( + props.storageKey, + JSON.stringify({ + files: filesToSave, + ts: Date.now(), + }), + ); + } catch (e) { + console.error('Failed to save reviewed files:', e); + } + }; + + const pruneStale = () => { + const now = Date.now(); + const REVIEWED_PREFIX = 'reviewed:'; + const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(REVIEWED_PREFIX) && key !== props.storageKey) { + try { + const entry = JSON.parse(localStorage.getItem(key) || '{}'); + if (!entry.ts || now - entry.ts > MAX_AGE_MS) { + localStorage.removeItem(key); + } + } catch { + localStorage.removeItem(key); + } + } + } + }; + + createEffect(() => { + if (Math.random() < 0.1) { + pruneStale(); + } + }); + + const [localOpen, setLocalOpen] = createSignal>({}); + const [hasSetInitial, setHasSetInitial] = createSignal(false); + + createEffect(() => { + if (files().length > 0) { + const isExpanded = expanded(); + const next: Record = {}; + for (const f of files()) { + if (!hasSetInitial() && reviewed().has(f.path)) { + next[f.path] = false; + } else { + next[f.path] = isExpanded; + } + } + setLocalOpen(next); + setHasSetInitial(true); + } + }); + + const toggleReview = (path: string) => { + const nextReviewed = new Set(reviewed()); + if (nextReviewed.has(path)) { + nextReviewed.delete(path); + } else { + nextReviewed.add(path); + setLocalOpen((prev) => ({ ...prev, [path]: false })); + } + setReviewed(nextReviewed); + saveReviewed(nextReviewed); + }; + return (
- + + 0 + ? 'text-green-600 dark:text-green-400' + : 'text-gray-600 dark:text-gray-400' + )} + > + {reviewed().size > 0 + ? `${reviewed().size}/${files().length} file${files().length === 1 ? '' : 's'} reviewed` + : `${files().length} changed file${files().length === 1 ? '' : 's'}` + } +