LinkWindow, VirtualScroller: virtualise correctly

This commit is contained in:
Liam Fitzgerald 2021-01-18 16:30:05 +10:00
parent 742971fec9
commit 959c7ee488
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
4 changed files with 77 additions and 48 deletions

View File

@ -60,18 +60,15 @@ export function LinkResource(props: LinkResourceProps) {
}
return (
<Col alignItems="center" height="100%" width="100%" overflowY="auto">
<Col alignItems="center" height="100%" width="100%" overflowY="hidden">
<Switch>
<Route
exact
path={relativePath("")}
render={(props) => {
return (
<Col width="100%" p={4} alignItems="center" maxWidth="768px">
<Col width="100%" flexShrink='0'>
<LinkSubmit s3={s3} name={name} ship={ship.slice(1)} api={api} />
</Col>
<LinkWindow
s3={s3}
association={resource}
contacts={contacts}
resource={resourcePath}
@ -83,7 +80,6 @@ export function LinkResource(props: LinkResourceProps) {
api={api}
mb={3}
/>
</Col>
);
}}
/>

View File

@ -1,4 +1,6 @@
import React, { useRef, useCallback, useEffect } from "react";
import React, { useRef, useCallback, useEffect, useMemo } from "react";
import { Col } from "@tlon/indigo-react";
import bigInt from 'big-integer';
import {
Association,
Graph,
@ -7,10 +9,12 @@ import {
LocalUpdateRemoteContentPolicy,
Group,
Rolodex,
S3State,
} from "~/types";
import GlobalApi from "~/logic/api/global";
import VirtualScroller from "~/views/components/VirtualScroller";
import { LinkItem } from "./components/LinkItem";
import LinkSubmit from "./components/LinkSubmit";
interface LinkWindowProps {
association: Association;
@ -24,6 +28,7 @@ interface LinkWindowProps {
group: Group;
path: string;
api: GlobalApi;
s3: S3State;
}
export function LinkWindow(props: LinkWindowProps) {
const { graph, api, association } = props;
@ -42,12 +47,24 @@ export function LinkWindow(props: LinkWindowProps) {
list.calculateVisibleItems();
}, [graph.size]);
const first = graph.peekLargest()?.[0];
const [,,ship, name] = association['app-path'].split('/');
const style = useMemo(() =>
({
height: "100%",
width: "100%",
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}), []);
return (
<VirtualScroller
ref={(l) => (virtualList.current = l ?? undefined)}
origin="top"
style={{ height: "100%", width: "100%" }}
style={style}
onStartReached={() => {}}
onScroll={() => {}}
data={graph}
@ -59,8 +76,17 @@ export function LinkWindow(props: LinkWindowProps) {
const linkProps = {
...props,
node,
measure,
key: index.toString()
};
if(index.eq(first ?? bigInt.zero)) {
return (
<Col mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink='0'>
<LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} />
<LinkItem {...linkProps} />
</Col>
)
}
return <LinkItem {...linkProps} />;
}}
loadRows={fetchLinks}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
@ -19,6 +19,7 @@ interface LinkItemProps {
path: string;
contacts: Rolodex;
unreads: Unreads;
measure: (el: any) => void;
}
export const LinkItem = (props: LinkItemProps) => {
@ -29,9 +30,12 @@ export const LinkItem = (props: LinkItemProps) => {
group,
path,
contacts,
measure,
...rest
} = props;
const ref = useRef<HTMLDivElement | null>(null);
const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
);
@ -70,9 +74,18 @@ export const LinkItem = (props: LinkItemProps) => {
const markRead = () => {
api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link');
}
return (
<Box width="100%" {...rest}>
const onMeasure = useCallback(() => {
ref.current && measure(ref.current);
}, [ref.current, measure])
useEffect(() => {
onMeasure();
}, [onMeasure]);
return (
<Box mx="auto" maxWidth="768px" ref={ref} width="100%" {...rest}>
<Box
lineHeight="tall"
display='flex'
@ -90,6 +103,7 @@ export const LinkItem = (props: LinkItemProps) => {
url={contents[1].url}
text={contents[0].text}
unfold={true}
onLoad={onMeasure}
style={{ alignSelf: 'center' }}
oembedProps={{
p: 2,

View File

@ -44,6 +44,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
timeout: ReturnType<typeof setTimeout>;
} | undefined;
overscan = 150;
OVERSCAN_SIZE = 100; // Minimum number of messages on either side before loadRows is called
constructor(props: VirtualScrollerProps) {
@ -53,7 +55,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
visibleItems: new BigIntOrderedMap(),
endgap: props.origin === 'bottom' ? 0 : undefined,
totalHeight: 0,
averageHeight: 64,
averageHeight: 130,
scrollTop: props.origin === 'top' ? 0 : undefined
};
@ -61,8 +63,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
this.window = null;
this.cache = new BigIntOrderedMap();
this.recalculateTotalHeight = this.recalculateTotalHeight.bind(this);
this.calculateVisibleItems = this.calculateVisibleItems.bind(this);
this.recalculateTotalHeight = _.throttle(this.recalculateTotalHeight.bind(this), 200);
this.calculateVisibleItems = _.throttle(this.calculateVisibleItems.bind(this), 200);
this.estimateIndexFromScrollTop = this.estimateIndexFromScrollTop.bind(this);
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
this.heightOf = this.heightOf.bind(this);
@ -74,6 +76,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
componentDidMount() {
this.calculateVisibleItems();
this.recalculateTotalHeight();
}
componentDidUpdate(prevProps: VirtualScrollerProps, prevState: VirtualScrollerState) {
@ -107,7 +111,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
let { averageHeight } = this.state;
let totalHeight = 0;
this.props.data.forEach((datum, index) => {
totalHeight += this.heightOf(index);
totalHeight += Math.max(this.heightOf(index), 0);
});
averageHeight = Number((totalHeight / this.props.data.size).toFixed());
totalHeight += (this.props.size - this.props.data.size) * averageHeight;
@ -136,36 +140,23 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
let startgap = 0, heightShown = 0, endgap = 0;
let startGapFilled = false;
let visibleItems = new BigIntOrderedMap<any>();
let startBuffer = new BigIntOrderedMap<any>();
let endBuffer = new BigIntOrderedMap<any>();
const { scrollTop, offsetHeight: windowHeight } = this.window;
const { averageHeight } = this.state;
const { averageHeight, totalHeight } = this.state;
const { data, size: totalSize, onCalculateVisibleItems } = this.props;
const overscan = Math.max(windowHeight / 2, 200);
[...data].forEach(([index, datum]) => {
const height = this.heightOf(index);
if (startgap < (scrollTop - overscan) && !startGapFilled) {
startBuffer.set(index, datum);
if (startgap < (scrollTop - this.overscan) && !startGapFilled) {
startgap += height;
} else if (heightShown < (windowHeight + overscan)) {
} else if (heightShown < (windowHeight + this.overscan)) {
startGapFilled = true;
visibleItems.set(index, datum);
heightShown += height;
} else if (endBuffer.size < visibleItems.size) {
endBuffer.set(index, data.get(index));
} else {
endgap += height;
}
});
startBuffer.forEach((_datum, index) => {
startgap -= this.heightOf(index);
});
endgap = totalHeight - heightShown - startgap;
const firstVisibleKey = visibleItems.peekSmallest()?.[0] ?? this.estimateIndexFromScrollTop(scrollTop)!;
const smallest = data.peekSmallest();
@ -184,7 +175,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
onCalculateVisibleItems ? onCalculateVisibleItems(visibleItems) : null;
this.setState({
startgap: Number(startgap.toFixed()),
visibleItems: new BigIntOrderedMap([...startBuffer, ...visibleItems, ...endBuffer]),
visibleItems,
endgap: Number(endgap.toFixed()),
});
}
@ -233,6 +224,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
}
}
this.overscan = Math.max(element.offsetHeight * 3, 500);
this.window = element;
if (this.props.origin === 'bottom') {
element.addEventListener('wheel', (event) => {
@ -309,7 +302,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
height: element.offsetHeight,
element
});
_.debounce(this.recalculateTotalHeight, 500)();
this.recalculateTotalHeight();
}
};
return renderer({ index, measure, scrollWindow: this.window });
@ -317,7 +310,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
return (
<Box overflowY='scroll' ref={this.setWindow.bind(this)} onScroll={this.onScroll.bind(this)} style={{ ...style, ...{ transform } }}>
<Box ref={this.scrollContainer} style={{ transform }}>
<Box ref={this.scrollContainer} style={{ transform, width: '100%' }}>
<Box style={{ height: `${origin === 'top' ? startgap : endgap}px` }}></Box>
{indexesToRender.map(render)}
<Box style={{ height: `${origin === 'top' ? endgap : startgap}px` }}></Box>