1
1
/*
2
- Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
2
+ Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
3
3
4
4
Licensed under the Apache License, Version 2.0 (the "License");
5
5
you may not use this file except in compliance with the License.
@@ -30,8 +30,8 @@ const UNPAGINATION_PADDING = 6000;
30
30
// The number of milliseconds to debounce calls to onUnfillRequest,
31
31
// to prevent many scroll events causing many unfilling requests.
32
32
const UNFILL_REQUEST_DEBOUNCE_MS = 200 ;
33
- // updateHeight makes the height a ceiled multiple of this so we don't have to update the height too often.
34
- // It also allows the user to scroll past the pagination spinner a bit so they don't feel blocked so
33
+ // updateHeight makes the height a `Math.ceil` multiple of this, so we don't have to update the height too often.
34
+ // It also allows the user to scroll past the pagination spinner a bit, so they don't feel blocked so
35
35
// much while the content loads.
36
36
const PAGE_SIZE = 400 ;
37
37
@@ -134,7 +134,7 @@ interface IProps {
134
134
*
135
135
* - fixed, in which the viewport is conceptually tied at a specific scroll
136
136
* offset. We don't save the absolute scroll offset, because that would be
137
- * affected by window width, zoom level, amount of scrollback, etc. Instead
137
+ * affected by window width, zoom level, amount of scrollback, etc. Instead,
138
138
* we save an identifier for the last fully-visible message, and the number
139
139
* of pixels the window was scrolled below it - which is hopefully near
140
140
* enough.
@@ -161,7 +161,8 @@ interface IPreventShrinkingState {
161
161
}
162
162
163
163
export default class ScrollPanel extends React . Component < IProps > {
164
- static defaultProps = {
164
+ // noinspection JSUnusedLocalSymbols
165
+ public static defaultProps = {
165
166
stickyBottom : true ,
166
167
startAtBottom : true ,
167
168
onFillRequest : function ( backwards : boolean ) { return Promise . resolve ( false ) ; } ,
@@ -200,21 +201,21 @@ export default class ScrollPanel extends React.Component<IProps> {
200
201
this . resetScrollState ( ) ;
201
202
}
202
203
203
- componentDidMount ( ) {
204
+ public componentDidMount ( ) : void {
204
205
this . checkScroll ( ) ;
205
206
}
206
207
207
- componentDidUpdate ( ) {
208
+ public componentDidUpdate ( ) : void {
208
209
// after adding event tiles, we may need to tweak the scroll (either to
209
210
// keep at the bottom of the timeline, or to maintain the view after
210
211
// adding events to the top).
211
212
//
212
- // This will also re-check the fill state, in case the paginate was inadequate
213
+ // This will also re-check the fill state, in case the pagination was inadequate
213
214
this . checkScroll ( true ) ;
214
215
this . updatePreventShrinking ( ) ;
215
216
}
216
217
217
- componentWillUnmount ( ) {
218
+ public componentWillUnmount ( ) : void {
218
219
// set a boolean to say we've been unmounted, which any pending
219
220
// promises can use to throw away their results.
220
221
//
@@ -224,19 +225,20 @@ export default class ScrollPanel extends React.Component<IProps> {
224
225
this . props . resizeNotifier ?. removeListener ( "middlePanelResizedNoisy" , this . onResize ) ;
225
226
}
226
227
227
- private onScroll = ev => {
228
+ private onScroll = ( ev : Event | React . UIEvent ) : void => {
228
229
// skip scroll events caused by resizing
229
230
if ( this . props . resizeNotifier && this . props . resizeNotifier . isResizing ) return ;
230
- debuglog ( "onScroll" , this . getScrollNode ( ) . scrollTop ) ;
231
+ debuglog ( "onScroll called past resize gate; scroll node top: " , this . getScrollNode ( ) . scrollTop ) ;
231
232
this . scrollTimeout . restart ( ) ;
232
233
this . saveScrollState ( ) ;
233
234
this . updatePreventShrinking ( ) ;
234
- this . props . onScroll ( ev ) ;
235
+ this . props . onScroll ?.( ev as Event ) ;
236
+ // noinspection JSIgnoredPromiseFromCall
235
237
this . checkFillState ( ) ;
236
238
} ;
237
239
238
- private onResize = ( ) => {
239
- debuglog ( "onResize" ) ;
240
+ private onResize = ( ) : void => {
241
+ debuglog ( "onResize called " ) ;
240
242
this . checkScroll ( ) ;
241
243
// update preventShrinkingState if present
242
244
if ( this . preventShrinkingState ) {
@@ -246,11 +248,14 @@ export default class ScrollPanel extends React.Component<IProps> {
246
248
247
249
// after an update to the contents of the panel, check that the scroll is
248
250
// where it ought to be, and set off pagination requests if necessary.
249
- public checkScroll = ( isFromPropsUpdate = false ) => {
251
+ public checkScroll = ( isFromPropsUpdate = false ) : void => {
250
252
if ( this . unmounted ) {
251
253
return ;
252
254
}
255
+ // We don't care if these two conditions race - they're different trees.
256
+ // noinspection JSIgnoredPromiseFromCall
253
257
this . restoreSavedScrollState ( ) ;
258
+ // noinspection JSIgnoredPromiseFromCall
254
259
this . checkFillState ( 0 , isFromPropsUpdate ) ;
255
260
} ;
256
261
@@ -259,7 +264,7 @@ export default class ScrollPanel extends React.Component<IProps> {
259
264
// note that this is independent of the 'stuckAtBottom' state - it is simply
260
265
// about whether the content is scrolled down right now, irrespective of
261
266
// whether it will stay that way when the children update.
262
- public isAtBottom = ( ) => {
267
+ public isAtBottom = ( ) : boolean => {
263
268
const sn = this . getScrollNode ( ) ;
264
269
// fractional values (both too big and too small)
265
270
// for scrollTop happen on certain browsers/platforms
@@ -277,7 +282,7 @@ export default class ScrollPanel extends React.Component<IProps> {
277
282
278
283
// returns the vertical height in the given direction that can be removed from
279
284
// the content box (which has a height of scrollHeight, see checkFillState) without
280
- // pagination occuring .
285
+ // pagination occurring .
281
286
//
282
287
// padding* = UNPAGINATION_PADDING
283
288
//
@@ -329,7 +334,7 @@ export default class ScrollPanel extends React.Component<IProps> {
329
334
const isFirstCall = depth === 0 ;
330
335
const sn = this . getScrollNode ( ) ;
331
336
332
- // if there is less than a screenful of messages above or below the
337
+ // if there is less than a screen's worth of messages above or below the
333
338
// viewport, try to get some more messages.
334
339
//
335
340
// scrollTop is the number of pixels between the top of the content and
@@ -408,6 +413,7 @@ export default class ScrollPanel extends React.Component<IProps> {
408
413
const refillDueToPropsUpdate = this . pendingFillDueToPropsUpdate ;
409
414
this . fillRequestWhileRunning = false ;
410
415
this . pendingFillDueToPropsUpdate = false ;
416
+ // noinspection ES6MissingAwait
411
417
this . checkFillState ( 0 , refillDueToPropsUpdate ) ;
412
418
}
413
419
} ;
@@ -424,7 +430,7 @@ export default class ScrollPanel extends React.Component<IProps> {
424
430
const tiles = this . itemlist . current . children ;
425
431
426
432
// The scroll token of the first/last tile to be unpaginated
427
- let markerScrollToken = null ;
433
+ let markerScrollToken : string | null = null ;
428
434
429
435
// Subtract heights of tiles to simulate the tiles being unpaginated until the
430
436
// excess height is less than the height of the next tile to subtract. This
@@ -434,7 +440,7 @@ export default class ScrollPanel extends React.Component<IProps> {
434
440
// If backwards is true, we unpaginate (remove) tiles from the back (top).
435
441
let tile ;
436
442
for ( let i = 0 ; i < tiles . length ; i ++ ) {
437
- tile = tiles [ backwards ? i : tiles . length - 1 - i ] ;
443
+ tile = tiles [ backwards ? i : ( tiles . length - 1 - i ) ] ;
438
444
// Subtract height of tile as if it were unpaginated
439
445
excessHeight -= tile . clientHeight ;
440
446
//If removing the tile would lead to future pagination, break before setting scroll token
@@ -455,8 +461,8 @@ export default class ScrollPanel extends React.Component<IProps> {
455
461
}
456
462
this . unfillDebouncer = setTimeout ( ( ) => {
457
463
this . unfillDebouncer = null ;
458
- debuglog ( "unfilling now" , backwards , origExcessHeight ) ;
459
- this . props . onUnfillRequest ( backwards , markerScrollToken ) ;
464
+ debuglog ( "unfilling now" , { backwards, origExcessHeight } ) ;
465
+ this . props . onUnfillRequest ?. ( backwards , markerScrollToken ! ) ;
460
466
} , UNFILL_REQUEST_DEBOUNCE_MS ) ;
461
467
}
462
468
}
@@ -465,11 +471,11 @@ export default class ScrollPanel extends React.Component<IProps> {
465
471
private maybeFill ( depth : number , backwards : boolean ) : Promise < void > {
466
472
const dir = backwards ? 'b' : 'f' ;
467
473
if ( this . pendingFillRequests [ dir ] ) {
468
- debuglog ( "Already a " + dir + " fill in progress - not starting another" ) ;
469
- return ;
474
+ debuglog ( "Already a fill in progress - not starting another; direction=" , dir ) ;
475
+ return Promise . resolve ( ) ;
470
476
}
471
477
472
- debuglog ( "starting " + dir + " fill" ) ;
478
+ debuglog ( "starting fill; direction=" , dir ) ;
473
479
474
480
// onFillRequest can end up calling us recursively (via onScroll
475
481
// events) so make sure we set this before firing off the call.
@@ -490,7 +496,7 @@ export default class ScrollPanel extends React.Component<IProps> {
490
496
// Unpaginate once filling is complete
491
497
this . checkUnfillState ( ! backwards ) ;
492
498
493
- debuglog ( "" + dir + " fill complete; hasMoreResults:" + hasMoreResults ) ;
499
+ debuglog ( "fill complete; hasMoreResults=" , hasMoreResults , "direction=" , dir ) ;
494
500
if ( hasMoreResults ) {
495
501
// further pagination requests have been disabled until now, so
496
502
// it's time to check the fill state again in case the pagination
@@ -562,11 +568,12 @@ export default class ScrollPanel extends React.Component<IProps> {
562
568
/**
563
569
* Page up/down.
564
570
*
565
- * @param {number } mult : -1 to page up, +1 to page down
571
+ * @param {number } multiple : -1 to page up, +1 to page down
566
572
*/
567
- public scrollRelative = ( mult : number ) : void => {
573
+ public scrollRelative = ( multiple : - 1 | 1 ) : void => {
568
574
const scrollNode = this . getScrollNode ( ) ;
569
- const delta = mult * scrollNode . clientHeight * 0.9 ;
575
+ // TODO: Document what magic number 0.9 is doing
576
+ const delta = multiple * scrollNode . clientHeight * 0.9 ;
570
577
scrollNode . scrollBy ( 0 , delta ) ;
571
578
this . saveScrollState ( ) ;
572
579
} ;
@@ -608,7 +615,7 @@ export default class ScrollPanel extends React.Component<IProps> {
608
615
pixelOffset = pixelOffset || 0 ;
609
616
offsetBase = offsetBase || 0 ;
610
617
611
- // set the trackedScrollToken so we can get the node through getTrackedNode
618
+ // set the trackedScrollToken, so we can get the node through getTrackedNode
612
619
this . scrollState = {
613
620
stuckAtBottom : false ,
614
621
trackedScrollToken : scrollToken ,
@@ -621,7 +628,7 @@ export default class ScrollPanel extends React.Component<IProps> {
621
628
// would position the trackedNode towards the top of the viewport.
622
629
// This because when setting the scrollTop only 10 or so events might be loaded,
623
630
// not giving enough content below the trackedNode to scroll downwards
624
- // enough so it ends up in the top of the viewport.
631
+ // enough, so it ends up in the top of the viewport.
625
632
debuglog ( "scrollToken: setting scrollTop" , { offsetBase, pixelOffset, offsetTop : trackedNode . offsetTop } ) ;
626
633
scrollNode . scrollTop = ( trackedNode . offsetTop - ( scrollNode . clientHeight * offsetBase ) ) + pixelOffset ;
627
634
this . saveScrollState ( ) ;
@@ -640,15 +647,16 @@ export default class ScrollPanel extends React.Component<IProps> {
640
647
641
648
const itemlist = this . itemlist . current ;
642
649
const messages = itemlist . children ;
643
- let node = null ;
650
+ let node : HTMLElement | null = null ;
644
651
645
652
// TODO: do a binary search here, as items are sorted by offsetTop
646
653
// loop backwards, from bottom-most message (as that is the most common case)
647
654
for ( let i = messages . length - 1 ; i >= 0 ; -- i ) {
648
- if ( ! ( messages [ i ] as HTMLElement ) . dataset . scrollTokens ) {
655
+ const htmlMessage = messages [ i ] as HTMLElement ;
656
+ if ( ! htmlMessage . dataset ?. scrollTokens ) { // dataset is only specified on HTMLElements
649
657
continue ;
650
658
}
651
- node = messages [ i ] ;
659
+ node = htmlMessage ;
652
660
// break at the first message (coming from the bottom)
653
661
// that has it's offsetTop above the bottom of the viewport.
654
662
if ( this . topFromBottom ( node ) > viewportBottom ) {
@@ -661,8 +669,8 @@ export default class ScrollPanel extends React.Component<IProps> {
661
669
debuglog ( "unable to save scroll state: found no children in the viewport" ) ;
662
670
return ;
663
671
}
664
- const scrollToken = node . dataset . scrollTokens . split ( ',' ) [ 0 ] ;
665
- debuglog ( "saving anchored scroll state to message" , node . innerText , scrollToken ) ;
672
+ const scrollToken = node ! . dataset . scrollTokens . split ( ',' ) [ 0 ] ;
673
+ debuglog ( "saving anchored scroll state to message" , scrollToken ) ;
666
674
const bottomOffset = this . topFromBottom ( node ) ;
667
675
this . scrollState = {
668
676
stuckAtBottom : false ,
@@ -714,12 +722,14 @@ export default class ScrollPanel extends React.Component<IProps> {
714
722
if ( this . scrollTimeout . isRunning ( ) ) {
715
723
debuglog ( "updateHeight waiting for scrolling to end ... " ) ;
716
724
await this . scrollTimeout . finished ( ) ;
725
+ debuglog ( "updateHeight actually running now" ) ;
717
726
} else {
718
- debuglog ( "updateHeight getting straight to business, no scrolling going on. " ) ;
727
+ debuglog ( "updateHeight running without delay " ) ;
719
728
}
720
729
721
730
// We might have unmounted since the timer finished, so abort if so.
722
731
if ( this . unmounted ) {
732
+ debuglog ( "updateHeight: abort due to unmount" ) ;
723
733
return ;
724
734
}
725
735
@@ -768,32 +778,32 @@ export default class ScrollPanel extends React.Component<IProps> {
768
778
}
769
779
}
770
780
771
- private getTrackedNode ( ) : HTMLElement {
781
+ private getTrackedNode ( ) : HTMLElement | undefined {
772
782
const scrollState = this . scrollState ;
773
783
const trackedNode = scrollState . trackedNode ;
774
784
775
785
if ( ! trackedNode ?. parentElement ) {
776
- let node : HTMLElement ;
786
+ let node : HTMLElement | undefined = undefined ;
777
787
const messages = this . itemlist . current . children ;
778
788
const scrollToken = scrollState . trackedScrollToken ;
779
789
780
790
for ( let i = messages . length - 1 ; i >= 0 ; -- i ) {
781
791
const m = messages [ i ] as HTMLElement ;
782
792
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
783
793
// There might only be one scroll token
784
- if ( m . dataset . scrollTokens ?. split ( ',' ) . includes ( scrollToken ) ) {
794
+ if ( scrollToken && m . dataset . scrollTokens ?. split ( ',' ) . includes ( scrollToken ! ) ) {
785
795
node = m ;
786
796
break ;
787
797
}
788
798
}
789
799
if ( node ) {
790
- debuglog ( "had to find tracked node again for " + scrollState . trackedScrollToken ) ;
800
+ debuglog ( "had to find tracked node again for token:" , scrollState . trackedScrollToken ) ;
791
801
}
792
802
scrollState . trackedNode = node ;
793
803
}
794
804
795
805
if ( ! scrollState . trackedNode ) {
796
- debuglog ( "No node with ; '" + scrollState . trackedScrollToken + "'" ) ;
806
+ debuglog ( "No node with token:" , scrollState . trackedScrollToken ) ;
797
807
return ;
798
808
}
799
809
@@ -842,7 +852,7 @@ export default class ScrollPanel extends React.Component<IProps> {
842
852
} ;
843
853
844
854
/**
845
- Mark the bottom offset of the last tile so we can balance it out when
855
+ Mark the bottom offset of the last tile, so we can balance it out when
846
856
anything below it changes, by calling updatePreventShrinking, to keep
847
857
the same minimum bottom offset, effectively preventing the timeline to shrink.
848
858
*/
@@ -921,7 +931,7 @@ export default class ScrollPanel extends React.Component<IProps> {
921
931
}
922
932
} ;
923
933
924
- render ( ) {
934
+ public render ( ) : ReactNode {
925
935
// TODO: the classnames on the div and ol could do with being updated to
926
936
// reflect the fact that we don't necessarily contain a list of messages.
927
937
// it's not obvious why we have a separate div and ol anyway.
0 commit comments