@@ -34,6 +34,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
3434import {
3535 buildPullRequestTimeline ,
3636 groupPullRequestTimelineConversations ,
37+ type PullRequestTimelineRow ,
3738 isPullRequestVerdictStale ,
3839 newestPullRequestCommitAt ,
3940 pullRequestReviewOutcome ,
@@ -56,6 +57,7 @@ import {
5657 pullRequestReviewOutcomeToneClassName ,
5758} from "./pullRequestPresentation" ;
5859import { PullRequestGlyph } from "./pullRequestIcons" ;
60+ import { LegendList , type LegendListRef } from "@legendapp/list/react" ;
5961
6062interface ReactionSurface {
6163 readonly canReact : boolean ;
@@ -634,7 +636,7 @@ export function PullRequestTimelineTab({
634636 onOpenCommit,
635637 onRefresh,
636638} : {
637- scrollerRef : ( node : HTMLDivElement | null ) => void ;
639+ scrollerRef : ( node : HTMLElement | null ) => void ;
638640 detail : PullRequestDetailView ;
639641 environmentId : EnvironmentId ;
640642 threadRef ?: ScopedThreadRef | null ;
@@ -666,66 +668,81 @@ export function PullRequestTimelineTab({
666668 void readLocalApi ( ) ?. shell . openExternal ( url ) ;
667669 } ;
668670
669- return (
670- < div ref = { scrollerRef } className = "scrollbar-gutter-both h-full overflow-y-auto px-4 py-5" >
671- < div className = "mx-auto max-w-3xl" >
672- < div className = "relative" >
673- < span aria-hidden className = "absolute bottom-5 left-[15px] top-1 w-px bg-border/45" />
674- { rows . map ( ( row ) => {
675- if ( row . kind === "comments" ) {
676- return (
677- < ConversationGroup
678- key = { `comments:${ row . events [ 0 ] ?. id ?? "empty" } ` }
679- events = { row . events }
680- editable = { editable }
681- cwd = { detail . workspaceRoot }
682- onOpen = { openOnHost }
683- reactions = { reactions }
684- />
685- ) ;
686- }
687- const event = row . event ;
688- if ( event . thread !== undefined ) {
689- return (
690- < ReviewThreadEvent
691- key = { `${ reactions . reference . projectId } #${ reactions . reference . number } :thread:${ event . id } :${ event . thread . isResolved } :${ pullRequestReviewOutcome ( event . reviewState ) === "dismissed" } ` }
692- event = { event }
693- thread = { event . thread }
694- editable = { editable }
695- cwd = { detail . workspaceRoot }
696- onOpen = { openOnHost }
697- reactions = { reactions }
698- />
699- ) ;
700- }
701- if ( event . kind === "commit" ) {
702- return < CommitEvent key = { event . id } event = { event } onOpen = { onOpenCommit } /> ;
703- }
704- const outcome = pullRequestReviewOutcome ( event . reviewState ) ;
705- if ( outcome !== null ) {
706- return (
707- < ReviewVerdictEvent
708- key = { event . id }
709- event = { event }
710- outcome = { outcome }
711- stale = { isPullRequestVerdictStale ( event . at , newestCommitAt ) }
712- cwd = { detail . workspaceRoot }
713- onOpen = { openOnHost }
714- reactions = { reactions }
715- />
716- ) ;
717- }
718- return < LifecycleEvent key = { event . id } event = { event } term = { term } /> ;
719- } ) }
720- </ div >
671+ const rowKey = ( row : PullRequestTimelineRow ) => {
672+ if ( row . kind === "comments" ) return `comments:${ row . events [ 0 ] ?. id ?? "empty" } ` ;
673+ const event = row . event ;
674+ // A thread's resolution or dismissal remounts it, so its disclosure starts in the new state.
675+ return event . thread === undefined
676+ ? event . id
677+ : `thread:${ event . id } :${ event . thread . isResolved } :${ pullRequestReviewOutcome ( event . reviewState ) === "dismissed" } ` ;
678+ } ;
679+ const renderRow = ( { item : row } : { item : PullRequestTimelineRow } ) => {
680+ if ( row . kind === "comments" ) {
681+ return (
682+ < ConversationGroup
683+ events = { row . events }
684+ editable = { editable }
685+ cwd = { detail . workspaceRoot }
686+ onOpen = { openOnHost }
687+ reactions = { reactions }
688+ />
689+ ) ;
690+ }
691+ const event = row . event ;
692+ if ( event . thread !== undefined ) {
693+ return (
694+ < ReviewThreadEvent
695+ event = { event }
696+ thread = { event . thread }
697+ editable = { editable }
698+ cwd = { detail . workspaceRoot }
699+ onOpen = { openOnHost }
700+ reactions = { reactions }
701+ />
702+ ) ;
703+ }
704+ if ( event . kind === "commit" ) return < CommitEvent event = { event } onOpen = { onOpenCommit } /> ;
705+ const outcome = pullRequestReviewOutcome ( event . reviewState ) ;
706+ if ( outcome !== null ) {
707+ return (
708+ < ReviewVerdictEvent
709+ event = { event }
710+ outcome = { outcome }
711+ stale = { isPullRequestVerdictStale ( event . at , newestCommitAt ) }
712+ cwd = { detail . workspaceRoot }
713+ onOpen = { openOnHost }
714+ reactions = { reactions }
715+ />
716+ ) ;
717+ }
718+ return < LifecycleEvent event = { event } term = { term } /> ;
719+ } ;
721720
722- { events . length === 0 ? (
723- < div className = "flex flex-col items-center justify-center py-16 text-center text-muted-foreground" >
724- < PullRequestGlyph . pullRequest className = "mb-2 size-5" />
725- < p className = "text-xs" > No activity yet.</ p >
726- </ div >
727- ) : null }
721+ if ( events . length === 0 ) {
722+ return (
723+ < div ref = { scrollerRef } className = "scrollbar-gutter-both h-full overflow-y-auto px-4 py-5" >
724+ < div className = "flex flex-col items-center justify-center py-16 text-center text-muted-foreground" >
725+ < PullRequestGlyph . pullRequest className = "mb-2 size-5" />
726+ < p className = "text-xs" > No activity yet.</ p >
727+ </ div >
728728 </ div >
729- </ div >
729+ ) ;
730+ }
731+
732+ // Virtualized: a large pull request has thousands of events, and mounting every one stalls
733+ // the page. The connector line is drawn by the content container, behind the rows.
734+ return (
735+ < LegendList < PullRequestTimelineRow >
736+ ref = { ( list : LegendListRef | null ) => scrollerRef ( list ?. getScrollableNode ( ) ?? null ) }
737+ data = { rows }
738+ extraData = { { editable, reactions, newestCommitAt } }
739+ keyExtractor = { rowKey }
740+ renderItem = { renderRow }
741+ estimatedItemSize = { 140 }
742+ className = "scrollbar-gutter-both h-full px-4"
743+ contentContainerClassName = "relative mx-auto w-full max-w-3xl before:absolute before:bottom-10 before:left-[15px] before:top-6 before:w-px before:bg-border/45"
744+ ListHeaderComponent = { < div className = "h-5" /> }
745+ ListFooterComponent = { < div className = "h-5" /> }
746+ />
730747 ) ;
731748}
0 commit comments