@@ -28,8 +28,8 @@ enum SegmentListTab {
2828 Chapter
2929}
3030
31- interface segmentWithNesting extends SponsorTime {
32- innerChapters ?: ( segmentWithNesting | SponsorTime ) [ ] ;
31+ interface SegmentWithNesting extends SponsorTime {
32+ innerChapters ?: ( SegmentWithNesting | SponsorTime ) [ ] ;
3333}
3434
3535export const SegmentListComponent = ( props : SegmentListComponentProps ) => {
@@ -58,37 +58,43 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
5858 }
5959 } ;
6060
61- const segmentsWithNesting : segmentWithNesting [ ] = [ ] ;
62- let nbTrailingNonChapters = 0 ;
63- function nestChapters ( segments : segmentWithNesting [ ] , seg : SponsorTime , topLevel ?: boolean ) {
64- if ( seg . actionType === ActionType . Chapter && segments . length ) {
65- // trailing non-chapters can only exist at top level
66- const lastElement = segments [ segments . length - ( topLevel ? nbTrailingNonChapters + 1 : 1 ) ]
67-
68- if ( lastElement . actionType === ActionType . Chapter
69- && lastElement . segment [ 0 ] <= seg . segment [ 0 ]
70- && lastElement . segment [ 1 ] >= seg . segment [ 1 ] ) {
71- if ( lastElement . innerChapters ) {
72- nestChapters ( lastElement . innerChapters , seg ) ;
73- } else {
74- lastElement . innerChapters = [ seg ] ;
75- }
76- } else {
77- if ( topLevel ) {
78- nbTrailingNonChapters = 0 ;
79- }
80-
81- segments . push ( seg ) ;
82- }
83- } else {
61+ const segmentsWithNesting = React . useMemo ( ( ) => {
62+ const result : SegmentWithNesting [ ] = [ ] ;
63+ const chapterStack : SegmentWithNesting [ ] = [ ] ;
64+ for ( let seg of props . segments ) {
65+ seg = { ...seg } ;
66+ // non-chapter, do not nest
8467 if ( seg . actionType !== ActionType . Chapter ) {
85- nbTrailingNonChapters ++ ;
68+ result . push ( seg ) ;
69+ continue ;
70+ }
71+ // traverse the stack
72+ while ( chapterStack . length !== 0 ) {
73+ // where's Array.prototype.at() :sob:
74+ const lastChapter = chapterStack [ chapterStack . length - 1 ] ;
75+ // we know lastChapter.startTime <= seg.startTime, as content.ts sorts these
76+ // so only compare endTime - if new ends before last, new is nested inside last
77+ if ( lastChapter . segment [ 1 ] >= seg . segment [ 1 ] ) {
78+ lastChapter . innerChapters ??= [ ] ;
79+ lastChapter . innerChapters . push ( seg ) ;
80+ chapterStack . push ( seg ) ;
81+ break ;
82+ }
83+ // last did not match, pop it off the stack
84+ chapterStack . pop ( ) ;
8685 }
86+ // chapter stack not empty = we found a place for the chapter
87+ if ( chapterStack . length !== 0 ) {
88+ continue ;
89+ }
90+ // push the chapter to the top-level list and to the stack
91+ result . push ( seg ) ;
92+ chapterStack . push ( seg ) ;
8793
88- segments . push ( seg ) ;
8994 }
90- }
91- props . segments . forEach ( ( seg ) => nestChapters ( segmentsWithNesting , { ...seg } , true ) ) ;
95+ return result ;
96+ } , [ props . segments ] )
97+
9298
9399 return (
94100 < div id = "issueReporterContainer" >
@@ -136,7 +142,7 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
136142} ;
137143
138144function SegmentListItem ( { segment, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage } : {
139- segment : segmentWithNesting ;
145+ segment : SegmentWithNesting ;
140146 videoID : VideoID ;
141147 currentTime : number ;
142148 isVip : boolean ;
@@ -146,18 +152,32 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
146152 sendMessage : ( request : Message ) => Promise < MessageResponse > ;
147153} ) {
148154 const [ voteMessage , setVoteMessage ] = React . useState < string | null > ( null ) ;
149- const [ hidden , setHidden ] = React . useState ( segment . hidden || SponsorHideType . Visible ) ;
155+ const [ hidden , setHidden ] = React . useState ( segment . hidden ?? SponsorHideType . Visible ) ; // undefined ?? undefined lol
150156 const [ isLooped , setIsLooped ] = React . useState ( loopedChapter === segment . UUID ) ;
151157
152- let extraInfo = "" ;
153- if ( segment . hidden === SponsorHideType . Downvoted ) {
154- // This one is downvoted
155- extraInfo = " (" + chrome . i18n . getMessage ( "hiddenDueToDownvote" ) + ")" ;
156- } else if ( segment . hidden === SponsorHideType . MinimumDuration ) {
157- // This one is too short
158- extraInfo = " (" + chrome . i18n . getMessage ( "hiddenDueToDuration" ) + ")" ;
159- } else if ( segment . hidden === SponsorHideType . Hidden ) {
160- extraInfo = " (" + chrome . i18n . getMessage ( "manuallyHidden" ) + ")" ;
158+ // Update internal state if the hidden property of the segment changes
159+ React . useEffect ( ( ) => {
160+ setHidden ( segment . hidden ?? SponsorHideType . Visible ) ;
161+ } , [ segment . hidden ] )
162+
163+ let extraInfo : string ;
164+ switch ( hidden ) {
165+ case SponsorHideType . Visible :
166+ extraInfo = "" ;
167+ break ;
168+ case SponsorHideType . Downvoted :
169+ extraInfo = " (" + chrome . i18n . getMessage ( "hiddenDueToDownvote" ) + ")" ;
170+ break ;
171+ case SponsorHideType . MinimumDuration :
172+ extraInfo = " (" + chrome . i18n . getMessage ( "hiddenDueToDuration" ) + ")" ;
173+ break ;
174+ case SponsorHideType . Hidden :
175+ extraInfo = " (" + chrome . i18n . getMessage ( "manuallyHidden" ) + ")" ;
176+ break ;
177+ default :
178+ // hidden satisfies never; // need to upgrade TS
179+ console . warn ( `[SB] Unhandled variant of SponsorHideType in SegmentListItem: ${ hidden } ` ) ;
180+ extraInfo = "" ;
161181 }
162182
163183 return (
@@ -279,7 +299,7 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
279299 {
280300 ( segment . actionType === ActionType . Skip || segment . actionType === ActionType . Mute
281301 || segment . actionType === ActionType . Poi
282- && [ SponsorHideType . Visible , SponsorHideType . Hidden ] . includes ( segment . hidden ) ) &&
302+ && [ SponsorHideType . Visible , SponsorHideType . Hidden ] . includes ( hidden ) ) &&
283303 < img
284304 className = "voteButton"
285305 title = { chrome . i18n . getMessage ( "hideSegment" ) }
@@ -288,17 +308,11 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
288308 const stopAnimation = AnimationUtils . applyLoadingAnimation ( e . currentTarget , 0.4 ) ;
289309 stopAnimation ( ) ;
290310
291- if ( segment . hidden === SponsorHideType . Hidden ) {
292- segment . hidden = SponsorHideType . Visible ;
293- setHidden ( SponsorHideType . Visible ) ;
294- } else {
295- segment . hidden = SponsorHideType . Hidden ;
296- setHidden ( SponsorHideType . Hidden ) ;
297- }
298-
311+ const newState = hidden === SponsorHideType . Hidden ? SponsorHideType . Visible : SponsorHideType . Hidden ;
312+ setHidden ( newState ) ;
299313 sendMessage ( {
300314 message : "hideSegment" ,
301- type : segment . hidden ,
315+ type : newState ,
302316 UUID : segment . UUID
303317 } ) ;
304318 } } />
@@ -343,7 +357,7 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
343357}
344358
345359function InnerChapterList ( { chapters, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage } : {
346- chapters : ( segmentWithNesting ) [ ] ;
360+ chapters : ( SegmentWithNesting ) [ ] ;
347361 videoID : VideoID ;
348362 currentTime : number ;
349363 isVip : boolean ;
0 commit comments