Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 09282d9

Browse files
authored
Minor code quality updates for ScrollPanel (#9639)
* Minor code quality updates for ScrollPanel and removal of unused function `scrollRelative` in MessagePanel (discovered when changing the scrollRelative types in ScrollPanel). Changes: * Clean up logging a bit, make it clearer * Remove dead code * Add extra types for tsc --strict compliance (not complete) * Fix IDE warnings around spelling and missing awaits/promise return values * Modernize usage of logging * Fix more tsc --strict errors while we're here * It's a good thing we have a linter * Fix even more strict errors
1 parent f642765 commit 09282d9

File tree

2 files changed

+55
-56
lines changed

2 files changed

+55
-56
lines changed

src/components/structures/MessagePanel.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ interface IProps {
183183
onFillRequest?(backwards: boolean): Promise<boolean>;
184184

185185
// helper function to access relations for an event
186-
onUnfillRequest?(backwards: boolean, scrollToken: string): void;
186+
onUnfillRequest?(backwards: boolean, scrollToken: string | null): void;
187187

188188
getRelationsForEvent?: GetRelationsForEvent;
189189

@@ -413,17 +413,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
413413
}
414414
}
415415

416-
/**
417-
* Page up/down.
418-
*
419-
* @param {number} mult: -1 to page up, +1 to page down
420-
*/
421-
public scrollRelative(mult: number): void {
422-
if (this.scrollPanel.current) {
423-
this.scrollPanel.current.scrollRelative(mult);
424-
}
425-
}
426-
427416
/**
428417
* Scroll up/down in response to a scroll key
429418
*

src/components/structures/ScrollPanel.tsx

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -30,8 +30,8 @@ const UNPAGINATION_PADDING = 6000;
3030
// The number of milliseconds to debounce calls to onUnfillRequest,
3131
// to prevent many scroll events causing many unfilling requests.
3232
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
3535
// much while the content loads.
3636
const PAGE_SIZE = 400;
3737

@@ -134,7 +134,7 @@ interface IProps {
134134
*
135135
* - fixed, in which the viewport is conceptually tied at a specific scroll
136136
* 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,
138138
* we save an identifier for the last fully-visible message, and the number
139139
* of pixels the window was scrolled below it - which is hopefully near
140140
* enough.
@@ -161,7 +161,8 @@ interface IPreventShrinkingState {
161161
}
162162

163163
export default class ScrollPanel extends React.Component<IProps> {
164-
static defaultProps = {
164+
// noinspection JSUnusedLocalSymbols
165+
public static defaultProps = {
165166
stickyBottom: true,
166167
startAtBottom: true,
167168
onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
@@ -200,21 +201,21 @@ export default class ScrollPanel extends React.Component<IProps> {
200201
this.resetScrollState();
201202
}
202203

203-
componentDidMount() {
204+
public componentDidMount(): void {
204205
this.checkScroll();
205206
}
206207

207-
componentDidUpdate() {
208+
public componentDidUpdate(): void {
208209
// after adding event tiles, we may need to tweak the scroll (either to
209210
// keep at the bottom of the timeline, or to maintain the view after
210211
// adding events to the top).
211212
//
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
213214
this.checkScroll(true);
214215
this.updatePreventShrinking();
215216
}
216217

217-
componentWillUnmount() {
218+
public componentWillUnmount(): void {
218219
// set a boolean to say we've been unmounted, which any pending
219220
// promises can use to throw away their results.
220221
//
@@ -224,19 +225,20 @@ export default class ScrollPanel extends React.Component<IProps> {
224225
this.props.resizeNotifier?.removeListener("middlePanelResizedNoisy", this.onResize);
225226
}
226227

227-
private onScroll = ev => {
228+
private onScroll = (ev: Event | React.UIEvent): void => {
228229
// skip scroll events caused by resizing
229230
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);
231232
this.scrollTimeout.restart();
232233
this.saveScrollState();
233234
this.updatePreventShrinking();
234-
this.props.onScroll(ev);
235+
this.props.onScroll?.(ev as Event);
236+
// noinspection JSIgnoredPromiseFromCall
235237
this.checkFillState();
236238
};
237239

238-
private onResize = () => {
239-
debuglog("onResize");
240+
private onResize = (): void => {
241+
debuglog("onResize called");
240242
this.checkScroll();
241243
// update preventShrinkingState if present
242244
if (this.preventShrinkingState) {
@@ -246,11 +248,14 @@ export default class ScrollPanel extends React.Component<IProps> {
246248

247249
// after an update to the contents of the panel, check that the scroll is
248250
// where it ought to be, and set off pagination requests if necessary.
249-
public checkScroll = (isFromPropsUpdate = false) => {
251+
public checkScroll = (isFromPropsUpdate = false): void => {
250252
if (this.unmounted) {
251253
return;
252254
}
255+
// We don't care if these two conditions race - they're different trees.
256+
// noinspection JSIgnoredPromiseFromCall
253257
this.restoreSavedScrollState();
258+
// noinspection JSIgnoredPromiseFromCall
254259
this.checkFillState(0, isFromPropsUpdate);
255260
};
256261

@@ -259,7 +264,7 @@ export default class ScrollPanel extends React.Component<IProps> {
259264
// note that this is independent of the 'stuckAtBottom' state - it is simply
260265
// about whether the content is scrolled down right now, irrespective of
261266
// whether it will stay that way when the children update.
262-
public isAtBottom = () => {
267+
public isAtBottom = (): boolean => {
263268
const sn = this.getScrollNode();
264269
// fractional values (both too big and too small)
265270
// for scrollTop happen on certain browsers/platforms
@@ -277,7 +282,7 @@ export default class ScrollPanel extends React.Component<IProps> {
277282

278283
// returns the vertical height in the given direction that can be removed from
279284
// the content box (which has a height of scrollHeight, see checkFillState) without
280-
// pagination occuring.
285+
// pagination occurring.
281286
//
282287
// padding* = UNPAGINATION_PADDING
283288
//
@@ -329,7 +334,7 @@ export default class ScrollPanel extends React.Component<IProps> {
329334
const isFirstCall = depth === 0;
330335
const sn = this.getScrollNode();
331336

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
333338
// viewport, try to get some more messages.
334339
//
335340
// 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> {
408413
const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate;
409414
this.fillRequestWhileRunning = false;
410415
this.pendingFillDueToPropsUpdate = false;
416+
// noinspection ES6MissingAwait
411417
this.checkFillState(0, refillDueToPropsUpdate);
412418
}
413419
};
@@ -424,7 +430,7 @@ export default class ScrollPanel extends React.Component<IProps> {
424430
const tiles = this.itemlist.current.children;
425431

426432
// The scroll token of the first/last tile to be unpaginated
427-
let markerScrollToken = null;
433+
let markerScrollToken: string | null = null;
428434

429435
// Subtract heights of tiles to simulate the tiles being unpaginated until the
430436
// 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> {
434440
// If backwards is true, we unpaginate (remove) tiles from the back (top).
435441
let tile;
436442
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)];
438444
// Subtract height of tile as if it were unpaginated
439445
excessHeight -= tile.clientHeight;
440446
//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> {
455461
}
456462
this.unfillDebouncer = setTimeout(() => {
457463
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!);
460466
}, UNFILL_REQUEST_DEBOUNCE_MS);
461467
}
462468
}
@@ -465,11 +471,11 @@ export default class ScrollPanel extends React.Component<IProps> {
465471
private maybeFill(depth: number, backwards: boolean): Promise<void> {
466472
const dir = backwards ? 'b' : 'f';
467473
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();
470476
}
471477

472-
debuglog("starting "+dir+" fill");
478+
debuglog("starting fill; direction=", dir);
473479

474480
// onFillRequest can end up calling us recursively (via onScroll
475481
// events) so make sure we set this before firing off the call.
@@ -490,7 +496,7 @@ export default class ScrollPanel extends React.Component<IProps> {
490496
// Unpaginate once filling is complete
491497
this.checkUnfillState(!backwards);
492498

493-
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
499+
debuglog("fill complete; hasMoreResults=", hasMoreResults, "direction=", dir);
494500
if (hasMoreResults) {
495501
// further pagination requests have been disabled until now, so
496502
// 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> {
562568
/**
563569
* Page up/down.
564570
*
565-
* @param {number} mult: -1 to page up, +1 to page down
571+
* @param {number} multiple: -1 to page up, +1 to page down
566572
*/
567-
public scrollRelative = (mult: number): void => {
573+
public scrollRelative = (multiple: -1 | 1): void => {
568574
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;
570577
scrollNode.scrollBy(0, delta);
571578
this.saveScrollState();
572579
};
@@ -608,7 +615,7 @@ export default class ScrollPanel extends React.Component<IProps> {
608615
pixelOffset = pixelOffset || 0;
609616
offsetBase = offsetBase || 0;
610617

611-
// set the trackedScrollToken so we can get the node through getTrackedNode
618+
// set the trackedScrollToken, so we can get the node through getTrackedNode
612619
this.scrollState = {
613620
stuckAtBottom: false,
614621
trackedScrollToken: scrollToken,
@@ -621,7 +628,7 @@ export default class ScrollPanel extends React.Component<IProps> {
621628
// would position the trackedNode towards the top of the viewport.
622629
// This because when setting the scrollTop only 10 or so events might be loaded,
623630
// 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.
625632
debuglog("scrollToken: setting scrollTop", { offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop });
626633
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
627634
this.saveScrollState();
@@ -640,15 +647,16 @@ export default class ScrollPanel extends React.Component<IProps> {
640647

641648
const itemlist = this.itemlist.current;
642649
const messages = itemlist.children;
643-
let node = null;
650+
let node: HTMLElement | null = null;
644651

645652
// TODO: do a binary search here, as items are sorted by offsetTop
646653
// loop backwards, from bottom-most message (as that is the most common case)
647654
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
649657
continue;
650658
}
651-
node = messages[i];
659+
node = htmlMessage;
652660
// break at the first message (coming from the bottom)
653661
// that has it's offsetTop above the bottom of the viewport.
654662
if (this.topFromBottom(node) > viewportBottom) {
@@ -661,8 +669,8 @@ export default class ScrollPanel extends React.Component<IProps> {
661669
debuglog("unable to save scroll state: found no children in the viewport");
662670
return;
663671
}
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);
666674
const bottomOffset = this.topFromBottom(node);
667675
this.scrollState = {
668676
stuckAtBottom: false,
@@ -714,12 +722,14 @@ export default class ScrollPanel extends React.Component<IProps> {
714722
if (this.scrollTimeout.isRunning()) {
715723
debuglog("updateHeight waiting for scrolling to end ... ");
716724
await this.scrollTimeout.finished();
725+
debuglog("updateHeight actually running now");
717726
} else {
718-
debuglog("updateHeight getting straight to business, no scrolling going on.");
727+
debuglog("updateHeight running without delay");
719728
}
720729

721730
// We might have unmounted since the timer finished, so abort if so.
722731
if (this.unmounted) {
732+
debuglog("updateHeight: abort due to unmount");
723733
return;
724734
}
725735

@@ -768,32 +778,32 @@ export default class ScrollPanel extends React.Component<IProps> {
768778
}
769779
}
770780

771-
private getTrackedNode(): HTMLElement {
781+
private getTrackedNode(): HTMLElement | undefined {
772782
const scrollState = this.scrollState;
773783
const trackedNode = scrollState.trackedNode;
774784

775785
if (!trackedNode?.parentElement) {
776-
let node: HTMLElement;
786+
let node: HTMLElement | undefined = undefined;
777787
const messages = this.itemlist.current.children;
778788
const scrollToken = scrollState.trackedScrollToken;
779789

780790
for (let i = messages.length - 1; i >= 0; --i) {
781791
const m = messages[i] as HTMLElement;
782792
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
783793
// There might only be one scroll token
784-
if (m.dataset.scrollTokens?.split(',').includes(scrollToken)) {
794+
if (scrollToken && m.dataset.scrollTokens?.split(',').includes(scrollToken!)) {
785795
node = m;
786796
break;
787797
}
788798
}
789799
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);
791801
}
792802
scrollState.trackedNode = node;
793803
}
794804

795805
if (!scrollState.trackedNode) {
796-
debuglog("No node with ; '"+scrollState.trackedScrollToken+"'");
806+
debuglog("No node with token:", scrollState.trackedScrollToken);
797807
return;
798808
}
799809

@@ -842,7 +852,7 @@ export default class ScrollPanel extends React.Component<IProps> {
842852
};
843853

844854
/**
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
846856
anything below it changes, by calling updatePreventShrinking, to keep
847857
the same minimum bottom offset, effectively preventing the timeline to shrink.
848858
*/
@@ -921,7 +931,7 @@ export default class ScrollPanel extends React.Component<IProps> {
921931
}
922932
};
923933

924-
render() {
934+
public render(): ReactNode {
925935
// TODO: the classnames on the div and ol could do with being updated to
926936
// reflect the fact that we don't necessarily contain a list of messages.
927937
// it's not obvious why we have a separate div and ol anyway.

0 commit comments

Comments
 (0)