mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +03:00
LinkWindow, VirtualScroller: virtualise correctly
This commit is contained in:
parent
742971fec9
commit
959c7ee488
@ -60,30 +60,26 @@ 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
|
||||
association={resource}
|
||||
contacts={contacts}
|
||||
resource={resourcePath}
|
||||
graph={graph}
|
||||
unreads={unreads}
|
||||
baseUrl={resourceUrl}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
api={api}
|
||||
mb={3}
|
||||
/>
|
||||
</Col>
|
||||
<LinkWindow
|
||||
s3={s3}
|
||||
association={resource}
|
||||
contacts={contacts}
|
||||
resource={resourcePath}
|
||||
graph={graph}
|
||||
unreads={unreads}
|
||||
baseUrl={resourceUrl}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
api={api}
|
||||
mb={3}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
||||
const onMeasure = useCallback(() => {
|
||||
ref.current && measure(ref.current);
|
||||
}, [ref.current, measure])
|
||||
|
||||
useEffect(() => {
|
||||
onMeasure();
|
||||
}, [onMeasure]);
|
||||
|
||||
return (
|
||||
<Box width="100%" {...rest}>
|
||||
|
||||
<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,
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user