Skip to content

Commit f799c14

Browse files
committed
Fix a bunch of tiny UI fixes
1 parent 4e7a3f5 commit f799c14

File tree

1 file changed

+153
-89
lines changed

1 file changed

+153
-89
lines changed

src/browser/components/pr-overview.tsx

Lines changed: 153 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
usePRReviewStore,
5858
getTimeAgo,
5959
} from "../contexts/pr-review";
60+
import { parseDiffCached, type ParsedDiff } from "../lib/diff";
6061
import {
6162
useGitHub,
6263
useCurrentUser,
@@ -145,13 +146,16 @@ export const PROverview = memo(function PROverview() {
145146

146147
// Repo permissions - use GraphQL viewerPermission as primary source
147148
const isArchived = pr.base?.repo?.archived ?? false;
148-
// WRITE, MAINTAIN, or ADMIN permissions allow merging
149-
const canPush =
149+
// WRITE, MAINTAIN, or ADMIN permissions allow merging and resolving threads
150+
const hasWritePermission =
150151
viewerPermission === "ADMIN" ||
151152
viewerPermission === "MAINTAIN" ||
152-
viewerPermission === "WRITE" ||
153-
pr.base?.repo?.permissions?.push === true;
153+
viewerPermission === "WRITE";
154+
const canPush =
155+
hasWritePermission || pr.base?.repo?.permissions?.push === true;
154156
const canMergeRepo = canWrite && canPush && !isArchived;
157+
// Resolving threads requires write permission to the repo
158+
const canResolveThread = canWrite && hasWritePermission;
155159

156160
// Reviewers and Assignees state
157161
const [collaborators, setCollaborators] = useState<
@@ -571,6 +575,52 @@ export const PROverview = memo(function PROverview() {
571575
[github, owner, repo]
572576
);
573577

578+
// Review comment reactions (different from issue comments)
579+
const handleAddReviewCommentReaction = useCallback(
580+
async (commentId: number, content: ReactionContent) => {
581+
try {
582+
const newReaction = await github.addReviewCommentReaction(
583+
owner,
584+
repo,
585+
commentId,
586+
content
587+
);
588+
setReactions((prev) => ({
589+
...prev,
590+
[`review-comment-${commentId}`]: [
591+
...(prev[`review-comment-${commentId}`] || []),
592+
newReaction,
593+
],
594+
}));
595+
} catch (error) {
596+
console.error("Failed to add review comment reaction:", error);
597+
}
598+
},
599+
[github, owner, repo]
600+
);
601+
602+
const handleRemoveReviewCommentReaction = useCallback(
603+
async (commentId: number, reactionId: number) => {
604+
try {
605+
await github.deleteReviewCommentReaction(
606+
owner,
607+
repo,
608+
commentId,
609+
reactionId
610+
);
611+
setReactions((prev) => ({
612+
...prev,
613+
[`review-comment-${commentId}`]: (
614+
prev[`review-comment-${commentId}`] || []
615+
).filter((r) => r.id !== reactionId),
616+
}));
617+
} catch (error) {
618+
console.error("Failed to remove review comment reaction:", error);
619+
}
620+
},
621+
[github, owner, repo]
622+
);
623+
574624
// Thread actions (reply, resolve, unresolve)
575625
const handleReplyToThread = useCallback(
576626
async (threadId: string, commentId: number, body: string) => {
@@ -734,7 +784,7 @@ export const PROverview = memo(function PROverview() {
734784
/>
735785

736786
{/* Timeline - merge comments, reviews, and events by date */}
737-
<div className="relative">
787+
<div className="relative space-y-6">
738788
{/* Vertical timeline line - z-0 so content appears above it */}
739789
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-muted-foreground/30 -translate-x-1/2 z-0" />
740790

@@ -893,7 +943,10 @@ export const PROverview = memo(function PROverview() {
893943
}
894944
if (entry.type === "review") {
895945
return (
896-
<div key={`review-${entry.data.id}`}>
946+
<div
947+
key={`review-${entry.data.id}`}
948+
className="space-y-3"
949+
>
897950
<ReviewBox review={entry.data} />
898951
{/* Render associated threads under the review */}
899952
{entry.threads.map((thread) => (
@@ -907,6 +960,18 @@ export const PROverview = memo(function PROverview() {
907960
onUnresolve={handleUnresolveThread}
908961
canWrite={canWrite}
909962
currentUser={currentUser}
963+
onAddReaction={handleAddReviewCommentReaction}
964+
onRemoveReaction={
965+
handleRemoveReviewCommentReaction
966+
}
967+
reactions={Object.fromEntries(
968+
thread.comments.nodes.map((c) => [
969+
c.databaseId,
970+
reactions[
971+
`review-comment-${c.databaseId}`
972+
] || [],
973+
])
974+
)}
910975
/>
911976
))}
912977
</div>
@@ -944,7 +1009,17 @@ export const PROverview = memo(function PROverview() {
9441009
onResolve={handleResolveThread}
9451010
onUnresolve={handleUnresolveThread}
9461011
canWrite={canWrite}
1012+
canResolveThread={canResolveThread}
9471013
currentUser={currentUser}
1014+
onAddReaction={handleAddReviewCommentReaction}
1015+
onRemoveReaction={handleRemoveReviewCommentReaction}
1016+
reactions={Object.fromEntries(
1017+
entry.data.comments.nodes.map((c) => [
1018+
c.databaseId,
1019+
reactions[`review-comment-${c.databaseId}`] ||
1020+
[],
1021+
])
1022+
)}
9481023
/>
9491024
);
9501025
}
@@ -2126,7 +2201,11 @@ function ReviewThreadBox({
21262201
onResolve,
21272202
onUnresolve,
21282203
canWrite,
2204+
canResolveThread,
21292205
currentUser,
2206+
onAddReaction,
2207+
onRemoveReaction,
2208+
reactions,
21302209
}: {
21312210
thread: ReviewThread;
21322211
owner: string;
@@ -2139,20 +2218,39 @@ function ReviewThreadBox({
21392218
onResolve?: (threadId: string) => Promise<void>;
21402219
onUnresolve?: (threadId: string) => Promise<void>;
21412220
canWrite?: boolean;
2221+
/** Whether user can resolve/unresolve threads (requires WRITE, MAINTAIN, or ADMIN permission) */
2222+
canResolveThread?: boolean;
21422223
currentUser?: string | null;
2224+
onAddReaction?: (commentId: number, content: ReactionContent) => void;
2225+
onRemoveReaction?: (commentId: number, reactionId: number) => void;
2226+
reactions?: Record<number, Reaction[]>;
21432227
}) {
21442228
const [replyText, setReplyText] = useState("");
21452229
const [submitting, setSubmitting] = useState(false);
21462230
const [resolving, setResolving] = useState(false);
21472231
const [showReplyBox, setShowReplyBox] = useState(false);
21482232
const [showResolved, setShowResolved] = useState(false);
2233+
const [parsedDiff, setParsedDiff] = useState<ParsedDiff | null>(null);
21492234
const comments = thread.comments.nodes;
21502235
if (comments.length === 0) return null;
21512236

21522237
const firstComment = comments[0];
21532238
const filePath = firstComment.path;
21542239
const diffHunk = firstComment.diffHunk;
21552240

2241+
// Parse diff hunk with syntax highlighting using the worker
2242+
// Note: The worker already adds git diff headers, so we pass diffHunk directly
2243+
useEffect(() => {
2244+
if (!diffHunk) {
2245+
setParsedDiff(null);
2246+
return;
2247+
}
2248+
2249+
parseDiffCached(diffHunk, filePath)
2250+
.then(setParsedDiff)
2251+
.catch(console.error);
2252+
}, [diffHunk, filePath]);
2253+
21562254
// If resolved and not expanded, show collapsed view
21572255
if (thread.isResolved && !showResolved) {
21582256
return (
@@ -2202,47 +2300,8 @@ function ReviewThreadBox({
22022300
}
22032301
};
22042302

2205-
// Parse diff hunk to get line numbers and content
2206-
const parseDiffHunk = (hunk: string | null) => {
2207-
if (!hunk) return [];
2208-
2209-
const lines = hunk.split("\n");
2210-
const result: Array<{
2211-
type: "header" | "context" | "addition" | "deletion";
2212-
content: string;
2213-
oldLine?: number;
2214-
newLine?: number;
2215-
}> = [];
2216-
2217-
let oldLine = 0;
2218-
let newLine = 0;
2219-
2220-
for (const line of lines) {
2221-
if (line.startsWith("@@")) {
2222-
const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
2223-
if (match) {
2224-
oldLine = parseInt(match[1], 10);
2225-
newLine = parseInt(match[2], 10);
2226-
}
2227-
result.push({ type: "header", content: line });
2228-
} else if (line.startsWith("+")) {
2229-
result.push({ type: "addition", content: line.slice(1), newLine });
2230-
newLine++;
2231-
} else if (line.startsWith("-")) {
2232-
result.push({ type: "deletion", content: line.slice(1), oldLine });
2233-
oldLine++;
2234-
} else {
2235-
const content = line.startsWith(" ") ? line.slice(1) : line;
2236-
result.push({ type: "context", content, oldLine, newLine });
2237-
oldLine++;
2238-
newLine++;
2239-
}
2240-
}
2241-
2242-
return result;
2243-
};
2244-
2245-
const diffLines = parseDiffHunk(diffHunk);
2303+
// Get diff lines from parsed diff (first hunk)
2304+
const diffHunkData = parsedDiff?.hunks.find((h) => h.type === "hunk") ?? null;
22462305

22472306
return (
22482307
<div
@@ -2274,53 +2333,46 @@ function ReviewThreadBox({
22742333
)}
22752334
</div>
22762335

2277-
{/* Code context (diff hunk) */}
2278-
{diffLines.length > 0 && (
2336+
{/* Code context (diff hunk) with syntax highlighting */}
2337+
{diffHunkData && diffHunkData.type === "hunk" && (
22792338
<div className="bg-[#0d1117] border-b border-border overflow-x-auto">
22802339
<table className="w-full text-xs font-mono">
22812340
<tbody>
2282-
{diffLines.map((line, i) => (
2341+
{diffHunkData.lines.map((line, i) => (
22832342
<tr
22842343
key={i}
22852344
className={cn(
2286-
line.type === "addition" && "bg-green-500/15",
2287-
line.type === "deletion" && "bg-red-500/15",
2288-
line.type === "header" && "bg-blue-500/10 text-blue-400"
2345+
line.type === "insert" && "bg-green-500/15",
2346+
line.type === "delete" && "bg-red-500/15"
22892347
)}
22902348
>
2291-
{line.type === "header" ? (
2292-
<td
2293-
colSpan={3}
2294-
className="px-2 py-0.5 text-muted-foreground"
2349+
<td className="w-10 text-right px-2 py-0.5 text-muted-foreground select-none border-r border-border/50">
2350+
{line.type !== "insert" ? line.oldLineNumber : ""}
2351+
</td>
2352+
<td className="w-10 text-right px-2 py-0.5 text-muted-foreground select-none border-r border-border/50">
2353+
{line.type !== "delete" ? line.newLineNumber : ""}
2354+
</td>
2355+
<td className="px-2 py-0.5 whitespace-pre">
2356+
<span
2357+
className={cn(
2358+
"select-none mr-1",
2359+
line.type === "insert" && "text-green-400",
2360+
line.type === "delete" && "text-red-400"
2361+
)}
22952362
>
2296-
{line.content}
2297-
</td>
2298-
) : (
2299-
<>
2300-
<td className="w-10 text-right px-2 py-0.5 text-muted-foreground select-none border-r border-border/50">
2301-
{line.type !== "addition" ? line.oldLine : ""}
2302-
</td>
2303-
<td className="w-10 text-right px-2 py-0.5 text-muted-foreground select-none border-r border-border/50">
2304-
{line.type !== "deletion" ? line.newLine : ""}
2305-
</td>
2306-
<td className="px-2 py-0.5 whitespace-pre">
2307-
<span
2308-
className={cn(
2309-
"select-none mr-1",
2310-
line.type === "addition" && "text-green-400",
2311-
line.type === "deletion" && "text-red-400"
2312-
)}
2313-
>
2314-
{line.type === "addition"
2315-
? "+"
2316-
: line.type === "deletion"
2317-
? "-"
2318-
: " "}
2319-
</span>
2320-
{line.content}
2321-
</td>
2322-
</>
2323-
)}
2363+
{line.type === "insert"
2364+
? "+"
2365+
: line.type === "delete"
2366+
? "-"
2367+
: " "}
2368+
</span>
2369+
{line.content.map((seg, j) => (
2370+
<span
2371+
key={j}
2372+
dangerouslySetInnerHTML={{ __html: seg.html }}
2373+
/>
2374+
))}
2375+
</td>
23242376
</tr>
23252377
))}
23262378
</tbody>
@@ -2362,11 +2414,23 @@ function ReviewThreadBox({
23622414
<div className="mt-2">
23632415
<Markdown>{comment.body}</Markdown>
23642416
</div>
2365-
{/* Emoji reactions placeholder */}
2417+
{/* Emoji reactions */}
23662418
<div className="mt-3">
2367-
<button className="inline-flex items-center justify-center w-7 h-7 text-xs rounded-full border border-border hover:border-blue-500/50 text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors">
2368-
<Smile className="w-4 h-4" />
2369-
</button>
2419+
<EmojiReactions
2420+
reactions={reactions?.[comment.databaseId] || []}
2421+
onAddReaction={
2422+
canWrite && onAddReaction
2423+
? (content) => onAddReaction(comment.databaseId, content)
2424+
: undefined
2425+
}
2426+
onRemoveReaction={
2427+
canWrite && onRemoveReaction
2428+
? (reactionId) =>
2429+
onRemoveReaction(comment.databaseId, reactionId)
2430+
: undefined
2431+
}
2432+
currentUser={currentUser}
2433+
/>
23702434
</div>
23712435
</div>
23722436
))}

0 commit comments

Comments
 (0)