@kitlangton/ghui 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/App.tsx +44 -5
- package/src/ui/DetailsPane.tsx +25 -17
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { Cause, Effect, Layer, Schedule } from "effect"
|
|
|
8
8
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
9
9
|
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
10
10
|
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
|
|
11
|
-
import { useContext, useEffect, useMemo, useRef, useState } from "react"
|
|
11
|
+
import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
12
12
|
import { buildAppCommands } from "./appCommands.js"
|
|
13
13
|
import type { AppCommand } from "./commands.js"
|
|
14
14
|
import { clampCommandIndex, commandEnabled, defineCommand, filterCommands, sortCommandsByScope } from "./commands.js"
|
|
@@ -603,6 +603,7 @@ export const App = () => {
|
|
|
603
603
|
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
604
604
|
const [startupLoadComplete, setStartupLoadComplete] = useState(false)
|
|
605
605
|
const [loadingMoreKey, setLoadingMoreKey] = useState<string | null>(null)
|
|
606
|
+
const [detailPreviewScrollTop, setDetailPreviewScrollTop] = useState(0)
|
|
606
607
|
const usernameResult = useAtomValue(usernameAtom)
|
|
607
608
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
608
609
|
const loadPullRequestPage = useAtomSet(listOpenPullRequestPageAtom, { mode: "promise" })
|
|
@@ -644,6 +645,7 @@ export const App = () => {
|
|
|
644
645
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
645
646
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
646
647
|
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
648
|
+
const detailPreviewScrollTopRef = useRef(0)
|
|
647
649
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
648
650
|
const prListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
649
651
|
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
@@ -1071,6 +1073,10 @@ export const App = () => {
|
|
|
1071
1073
|
setDiffPreferredSide(null)
|
|
1072
1074
|
setDiffCommentRangeStartIndex(null)
|
|
1073
1075
|
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1076
|
+
if (detailPreviewScrollTopRef.current !== 0) {
|
|
1077
|
+
detailPreviewScrollTopRef.current = 0
|
|
1078
|
+
setDetailPreviewScrollTop(0)
|
|
1079
|
+
}
|
|
1074
1080
|
}, [selectedIndex])
|
|
1075
1081
|
|
|
1076
1082
|
useEffect(() => {
|
|
@@ -1211,8 +1217,6 @@ export const App = () => {
|
|
|
1211
1217
|
title: `${loadingIndicator} Loading pull request details`,
|
|
1212
1218
|
hint: `${selectedPullRequest.repository} #${selectedPullRequest.number}`,
|
|
1213
1219
|
} : detailPlaceholderContent
|
|
1214
|
-
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
1215
|
-
|
|
1216
1220
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
1217
1221
|
|
|
1218
1222
|
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
@@ -1334,8 +1338,23 @@ export const App = () => {
|
|
|
1334
1338
|
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
1335
1339
|
}
|
|
1336
1340
|
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1341
|
+
const syncDetailPreviewScrollState = useCallback(() => {
|
|
1342
|
+
const scrollTop = detailPreviewScrollRef.current?.scrollTop
|
|
1343
|
+
if (scrollTop === undefined) return
|
|
1344
|
+
const nextTop = Math.max(0, Math.floor(scrollTop))
|
|
1345
|
+
if (detailPreviewScrollTopRef.current === nextTop) return
|
|
1346
|
+
detailPreviewScrollTopRef.current = nextTop
|
|
1347
|
+
setDetailPreviewScrollTop(nextTop)
|
|
1348
|
+
}, [])
|
|
1349
|
+
|
|
1350
|
+
const scrollDetailPreviewBy = (y: number) => {
|
|
1351
|
+
detailPreviewScrollRef.current?.scrollBy({ x: 0, y })
|
|
1352
|
+
syncDetailPreviewScrollState()
|
|
1353
|
+
}
|
|
1354
|
+
const scrollDetailPreviewTo = (y: number) => {
|
|
1355
|
+
detailPreviewScrollRef.current?.scrollTo({ x: 0, y })
|
|
1356
|
+
syncDetailPreviewScrollState()
|
|
1357
|
+
}
|
|
1339
1358
|
|
|
1340
1359
|
const ensureDiffLineVisible = (line: number) => {
|
|
1341
1360
|
const scroll = diffScrollRef.current
|
|
@@ -1354,6 +1373,16 @@ export const App = () => {
|
|
|
1354
1373
|
return () => globalThis.clearInterval(interval)
|
|
1355
1374
|
}, [diffFullView, stackedDiffFiles])
|
|
1356
1375
|
|
|
1376
|
+
useLayoutEffect(() => {
|
|
1377
|
+
if (!isWideLayout || detailFullView || diffFullView) return
|
|
1378
|
+
const scroll = detailPreviewScrollRef.current
|
|
1379
|
+
if (!scroll) return
|
|
1380
|
+
const sync = () => { syncDetailPreviewScrollState() }
|
|
1381
|
+
scroll.verticalScrollBar.on("change", sync)
|
|
1382
|
+
syncDetailPreviewScrollState()
|
|
1383
|
+
return () => { scroll.verticalScrollBar.off("change", sync) }
|
|
1384
|
+
}, [isWideLayout, detailFullView, diffFullView, syncDetailPreviewScrollState])
|
|
1385
|
+
|
|
1357
1386
|
const jumpDiffFile = (delta: 1 | -1) => {
|
|
1358
1387
|
if (readyDiffFiles.length === 0) return
|
|
1359
1388
|
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
@@ -2284,6 +2313,16 @@ export const App = () => {
|
|
|
2284
2313
|
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2285
2314
|
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2286
2315
|
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2316
|
+
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows({
|
|
2317
|
+
pullRequest: selectedPullRequest,
|
|
2318
|
+
paneWidth: rightPaneWidth,
|
|
2319
|
+
showChecks: true,
|
|
2320
|
+
contentWidth: rightContentWidth,
|
|
2321
|
+
conversationItems: selectedConversationItems,
|
|
2322
|
+
conversationStatus: selectedConversationStatus,
|
|
2323
|
+
bodyScrollTop: detailPreviewScrollTop,
|
|
2324
|
+
bodyViewportHeight: wideDetailBodyViewportHeight,
|
|
2325
|
+
})
|
|
2287
2326
|
|
|
2288
2327
|
const prListProps = {
|
|
2289
2328
|
groups: visibleGroups,
|
package/src/ui/DetailsPane.tsx
CHANGED
|
@@ -345,35 +345,43 @@ const ChecksSection = ({ checks, contentWidth }: { checks: readonly CheckItem[];
|
|
|
345
345
|
|
|
346
346
|
const conversationDividerBodyRow = (pullRequest: PullRequestItem, contentWidth: number, conversationItems: readonly PullRequestConversationItem[], conversationStatus: DetailConversationStatus) => {
|
|
347
347
|
if (!pullRequest.detailLoaded) return null
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
status: conversationStatus,
|
|
352
|
-
width: contentWidth,
|
|
353
|
-
limit: Math.max(0, DETAIL_BODY_SCROLL_LIMIT - summaryRows.length - 1),
|
|
354
|
-
})
|
|
355
|
-
return conversationRows.length > 0 ? summaryRows.length : null
|
|
348
|
+
const dividerIndex = detailBodyPreview({ pullRequest, contentWidth, limit: DETAIL_BODY_SCROLL_LIMIT, conversationItems, conversationStatus })
|
|
349
|
+
.findIndex((line) => line.divider === true)
|
|
350
|
+
return dividerIndex >= 0 ? dividerIndex : null
|
|
356
351
|
}
|
|
357
352
|
|
|
358
|
-
export const getDetailJunctionRows = (
|
|
359
|
-
pullRequest
|
|
360
|
-
paneWidth
|
|
353
|
+
export const getDetailJunctionRows = ({
|
|
354
|
+
pullRequest,
|
|
355
|
+
paneWidth,
|
|
361
356
|
showChecks = false,
|
|
362
|
-
contentWidth
|
|
363
|
-
conversationItems
|
|
364
|
-
conversationStatus
|
|
365
|
-
|
|
357
|
+
contentWidth,
|
|
358
|
+
conversationItems = [],
|
|
359
|
+
conversationStatus = "idle",
|
|
360
|
+
bodyScrollTop = 0,
|
|
361
|
+
bodyViewportHeight = Number.POSITIVE_INFINITY,
|
|
362
|
+
}: {
|
|
363
|
+
readonly pullRequest: PullRequestItem | null
|
|
364
|
+
readonly paneWidth: number
|
|
365
|
+
readonly showChecks?: boolean
|
|
366
|
+
readonly contentWidth?: number
|
|
367
|
+
readonly conversationItems?: readonly PullRequestConversationItem[]
|
|
368
|
+
readonly conversationStatus?: DetailConversationStatus
|
|
369
|
+
readonly bodyScrollTop?: number
|
|
370
|
+
readonly bodyViewportHeight?: number
|
|
371
|
+
}): readonly number[] => {
|
|
366
372
|
if (!pullRequest) return [DETAIL_PLACEHOLDER_ROWS]
|
|
373
|
+
const resolvedContentWidth = contentWidth ?? Math.max(1, paneWidth - 2)
|
|
367
374
|
const titleLines = wrapText(pullRequest.title, Math.max(1, paneWidth - 2)).length
|
|
368
375
|
const detailDividerRow = 1 + titleLines + 1
|
|
369
376
|
const checks = deduplicateChecks(pullRequest.checks)
|
|
370
377
|
const checksDividerRow = checks.length > 0 ? detailDividerRow + 1 + checksRowCount(checks) + 1 : -1
|
|
371
378
|
const headerHeight = getDetailHeaderHeight(pullRequest, paneWidth, showChecks)
|
|
372
|
-
const conversationDivider = conversationDividerBodyRow(pullRequest,
|
|
379
|
+
const conversationDivider = conversationDividerBodyRow(pullRequest, resolvedContentWidth, conversationItems, conversationStatus)
|
|
380
|
+
const visibleConversationDivider = conversationDivider === null ? null : conversationDivider - Math.max(0, Math.floor(bodyScrollTop))
|
|
373
381
|
return [
|
|
374
382
|
detailDividerRow,
|
|
375
383
|
showChecks && checks.length > 0 ? checksDividerRow : -1,
|
|
376
|
-
|
|
384
|
+
visibleConversationDivider === null || visibleConversationDivider < 0 || visibleConversationDivider >= bodyViewportHeight ? -1 : headerHeight + visibleConversationDivider,
|
|
377
385
|
].filter((row) => row >= 0)
|
|
378
386
|
}
|
|
379
387
|
|