mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 20:04:09 +03:00
Merge remote-tracking branch 'origin/release/next-userspace' into lf/bus-omni
This commit is contained in:
commit
b4da811fbe
@ -157,7 +157,7 @@
|
||||
++ to-range
|
||||
|= [item=byts f=@ k=byts]
|
||||
^- @
|
||||
(rsh [0 64] (mul f (swp 3 dat:(siphash k item))))
|
||||
(rsh [0 64] (mul f (rev 3 (siphash k item))))
|
||||
:: +set-construct: return sorted hashes of scriptpubkeys
|
||||
::
|
||||
++ set-construct
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
} from '@urbit/api';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import React, { Component } from 'react';
|
||||
import { GraphScroller } from '~/views/components/GraphScroller';
|
||||
import VirtualScroller from '~/views/components/VirtualScroller';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import UnreadNotice from './UnreadNotice';
|
||||
@ -45,7 +46,7 @@ class ChatWindow extends Component<
|
||||
ChatWindowProps,
|
||||
ChatWindowState
|
||||
> {
|
||||
private virtualList: VirtualScroller<GraphNode> | null;
|
||||
private virtualList: VirtualScroller<bigInt.BigInteger, GraphNode> | null;
|
||||
private prevSize = 0;
|
||||
private unreadSet = false;
|
||||
|
||||
@ -257,7 +258,7 @@ class ChatWindow extends Component<
|
||||
dismissUnread={this.props.dismissUnread}
|
||||
onClick={this.scrollToUnread}
|
||||
/>)}
|
||||
<VirtualScroller<GraphNode>
|
||||
<GraphScroller
|
||||
ref={(list) => {
|
||||
this.virtualList = list;
|
||||
}}
|
||||
|
@ -5,7 +5,7 @@ import React, {
|
||||
Component, ReactNode
|
||||
} from 'react';
|
||||
import { isWriter } from '~/logic/lib/group';
|
||||
import VirtualScroller from '~/views/components/VirtualScroller';
|
||||
import { GraphScroller } from '~/views/components/GraphScroller';
|
||||
import { LinkItem } from './components/LinkItem';
|
||||
import LinkSubmit from './components/LinkSubmit';
|
||||
|
||||
@ -124,7 +124,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
return (
|
||||
<Col width="100%" height="calc(100% - 48px)" position="relative">
|
||||
{/* @ts-ignore calling @liam-fitzgerald on virtualscroller */}
|
||||
<VirtualScroller
|
||||
<GraphScroller
|
||||
origin="top"
|
||||
offset={0}
|
||||
style={style}
|
||||
|
@ -8,9 +8,9 @@ import { LinkBlockInput } from './LinkBlockInput';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
import bigInt from 'big-integer';
|
||||
import VirtualScroller from '~/views/components/VirtualScroller';
|
||||
import airlock from '~/logic/api';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { BlockScroller } from '~/views/components/BlockScroller';
|
||||
|
||||
export interface LinkBlocksProps {
|
||||
graph: Graph;
|
||||
@ -112,7 +112,7 @@ export function LinkBlocks(props: LinkBlocksProps) {
|
||||
|
||||
return (
|
||||
<Col overflowX="hidden" overflowY="auto" height="calc(100% - 48px)" {...bind}>
|
||||
<VirtualScroller
|
||||
<BlockScroller
|
||||
origin="top"
|
||||
offset={0}
|
||||
style={style}
|
||||
|
@ -1,639 +0,0 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
import { Box, Center, LoadingSpinner } from '@tlon/indigo-react';
|
||||
import BigIntArrayOrderedMap, {
|
||||
arrToString,
|
||||
stringToArr
|
||||
} from '@urbit/api/lib/BigIntArrayOrderedMap';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import _ from 'lodash';
|
||||
import normalizeWheel from 'normalize-wheel';
|
||||
import React, { Component, SyntheticEvent, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { IS_IOS } from '~/logic/lib/platform';
|
||||
import { VirtualContext } from '~/logic/lib/virtualContext';
|
||||
import { clamp } from '~/logic/lib/util';
|
||||
|
||||
const ScrollbarLessBox = styled(Box)`
|
||||
scrollbar-width: none !important;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
interface RendererProps {
|
||||
index: BigInteger[];
|
||||
scrollWindow: any;
|
||||
ref: (el: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
export { arrToString, stringToArr };
|
||||
|
||||
export function indexEqual(a: BigInteger[], b: BigInteger[]) {
|
||||
const aLen = a.length;
|
||||
const bLen = b.length;
|
||||
|
||||
if (aLen === bLen) {
|
||||
let i = 0;
|
||||
while (i < aLen && i < bLen) {
|
||||
if (a[i].eq(b[i])) {
|
||||
if (i === aLen - 1) {
|
||||
return true;
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
interface VirtualScrollerProps<T> {
|
||||
/**
|
||||
* Start scroll from
|
||||
*/
|
||||
origin: 'top' | 'bottom';
|
||||
/**
|
||||
* Load more of the graph
|
||||
*
|
||||
* @returns boolean whether or not the graph is now fully loaded
|
||||
*/
|
||||
loadRows(newer: boolean): Promise<boolean>;
|
||||
/**
|
||||
* The data to iterate over
|
||||
*/
|
||||
data: BigIntArrayOrderedMap<T>;
|
||||
/**
|
||||
* The component to render the items
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This component must be referentially stable, so either use `useCallback` or
|
||||
* a instance method. It must also forward the DOM ref from its root DOM node
|
||||
*/
|
||||
renderer: (props: RendererProps) => JSX.Element | null;
|
||||
onStartReached?(): void;
|
||||
onEndReached?(): void;
|
||||
size: number;
|
||||
pendingSize: number;
|
||||
/**
|
||||
* Average height of a single rendered item
|
||||
*
|
||||
* @remarks
|
||||
* This is used primarily to calculate how many items should be onscreen. If
|
||||
* size is variable, err on the lower side.
|
||||
*/
|
||||
averageHeight: number;
|
||||
/**
|
||||
* The offset to begin rendering at, on load.
|
||||
*
|
||||
* @remarks
|
||||
* This is only looked up once, on component creation. Subsequent changes to
|
||||
* this prop will have no effect
|
||||
*/
|
||||
offset: number;
|
||||
style?: any;
|
||||
/**
|
||||
* Callback to execute when finished loading from start
|
||||
*/
|
||||
onBottomLoaded?: () => void;
|
||||
}
|
||||
|
||||
interface VirtualScrollerState {
|
||||
visibleItems: BigInteger[][];
|
||||
scrollbar: number;
|
||||
loaded: {
|
||||
top: boolean;
|
||||
bottom: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
type LogLevel = 'scroll' | 'network' | 'bail' | 'reflow';
|
||||
const logLevel = ['network', 'bail', 'scroll', 'reflow'] as LogLevel[];
|
||||
|
||||
const log = (level: LogLevel, message: string) => {
|
||||
if(logLevel.includes(level)) {
|
||||
console.log(`[${level}]: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const ZONE_SIZE = IS_IOS ? 20 : 80;
|
||||
|
||||
// nb: in this file, an index refers to a BigInteger[] and an offset refers to a
|
||||
// number used to index a listified BigIntArrayOrderedMap
|
||||
|
||||
/**
|
||||
* A virtualscroller for a `BigIntArrayOrderedMap`.
|
||||
*
|
||||
* VirtualScroller does not clean up or reset itself, so please use `key`
|
||||
* to ensure a new instance is created for each BigIntArrayOrderedMap
|
||||
*/
|
||||
export default class ArrayVirtualScroller<T> extends Component<VirtualScrollerProps<T>, VirtualScrollerState> {
|
||||
/**
|
||||
* A reference to our scroll container
|
||||
*/
|
||||
window: HTMLDivElement | null = null;
|
||||
/**
|
||||
* A map of child refs, used to calculate scroll position
|
||||
*/
|
||||
private childRefs = new Map<string, HTMLElement>();
|
||||
/**
|
||||
* A set of child refs which have been unmounted
|
||||
*/
|
||||
private orphans = new Set<string>();
|
||||
/**
|
||||
* If saving, the bottommost visible element that we pin our scroll to
|
||||
*/
|
||||
private savedIndex: BigInteger[] | null = null;
|
||||
/**
|
||||
* If saving, the distance between the top of `this.savedEl` and the bottom
|
||||
* of the screen
|
||||
*/
|
||||
private savedDistance = 0;
|
||||
|
||||
/**
|
||||
* If saving, the number of requested saves. If several images are loading
|
||||
* at once, we save the scroll pos the first time we see it and restore
|
||||
* once the number of requested saves is zero
|
||||
*/
|
||||
private saveDepth = 0;
|
||||
|
||||
scrollLocked = true;
|
||||
|
||||
private pageSize = 50;
|
||||
|
||||
private pageDelta = 15;
|
||||
|
||||
private scrollRef: HTMLElement | null = null;
|
||||
|
||||
private cleanupRefInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(props: VirtualScrollerProps<T>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visibleItems: [],
|
||||
scrollbar: 0,
|
||||
loaded: {
|
||||
top: false,
|
||||
bottom: false
|
||||
}
|
||||
};
|
||||
|
||||
this.updateVisible = this.updateVisible.bind(this);
|
||||
|
||||
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
||||
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 200) : this.onScroll.bind(this);
|
||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||
this.setWindow = this.setWindow.bind(this);
|
||||
this.restore = this.restore.bind(this);
|
||||
this.startOffset = this.startOffset.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateVisible(0);
|
||||
this.loadTop();
|
||||
this.loadBottom();
|
||||
this.cleanupRefInterval = setInterval(this.cleanupRefs, 5000);
|
||||
}
|
||||
|
||||
cleanupRefs = () => {
|
||||
if(this.saveDepth > 0) {
|
||||
return;
|
||||
}
|
||||
[...this.orphans].forEach((o) => {
|
||||
this.childRefs.delete(o);
|
||||
});
|
||||
this.orphans.clear();
|
||||
};
|
||||
|
||||
// manipulate scrollbar manually, to dodge change detection
|
||||
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
||||
if(!this.window || !this.scrollRef) {
|
||||
return;
|
||||
}
|
||||
const { scrollTop, scrollHeight } = this.window;
|
||||
|
||||
const unloaded = (this.startOffset() / this.pageSize);
|
||||
const totalpages = this.props.size / this.pageSize;
|
||||
|
||||
const loaded = (scrollTop / scrollHeight);
|
||||
const result = ((unloaded + loaded) / totalpages) * this.window.offsetHeight;
|
||||
this.scrollRef.style[this.props.origin] = `${result}px`;
|
||||
}, 50);
|
||||
|
||||
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState) {
|
||||
const { size, pendingSize } = this.props;
|
||||
|
||||
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
||||
if((this.window?.scrollTop ?? 0) < ZONE_SIZE) {
|
||||
this.scrollLocked = true;
|
||||
this.updateVisible(0);
|
||||
this.resetScroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||
if(this.cleanupRefInterval) {
|
||||
clearInterval(this.cleanupRefInterval);
|
||||
}
|
||||
this.cleanupRefs();
|
||||
this.childRefs.clear();
|
||||
}
|
||||
|
||||
startOffset() {
|
||||
const { data } = this.props;
|
||||
const startIndex = this.state.visibleItems?.[0];
|
||||
if(!startIndex) {
|
||||
return 0;
|
||||
}
|
||||
const dataList = Array.from(data);
|
||||
const offset = dataList.findIndex(([i]) => indexEqual(i, startIndex));
|
||||
if(offset === -1) {
|
||||
// TODO: revisit when we remove nodes for any other reason than
|
||||
// pending indices being removed
|
||||
return 0;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `startOffset` and adjusts visible items accordingly.
|
||||
* Saves the scroll positions before repainting and restores it afterwards
|
||||
*
|
||||
* @param newOffset new startOffset
|
||||
*/
|
||||
updateVisible(newOffset: number) {
|
||||
if (!this.window) {
|
||||
return;
|
||||
}
|
||||
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
||||
|
||||
const { data } = this.props;
|
||||
const visibleItems = data.keys().slice(newOffset, newOffset + this.pageSize);
|
||||
|
||||
this.save();
|
||||
|
||||
this.setState({
|
||||
visibleItems
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
this.restore();
|
||||
});
|
||||
}
|
||||
|
||||
scrollKeyMap(): Map<string, number> {
|
||||
return new Map([
|
||||
['ArrowUp', this.props.averageHeight],
|
||||
['ArrowDown', this.props.averageHeight * -1],
|
||||
['PageUp', this.window!.offsetHeight],
|
||||
['PageDown', this.window!.offsetHeight * -1],
|
||||
['Home', this.window!.scrollHeight],
|
||||
['End', this.window!.scrollHeight * -1],
|
||||
['Space', this.window!.offsetHeight * -1]
|
||||
]);
|
||||
}
|
||||
|
||||
invertedKeyHandler(event): void | false {
|
||||
const map = this.scrollKeyMap();
|
||||
if (map.has(event.code) && document.body.isSameNode(document.activeElement)) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
let distance = map.get(event.code)!;
|
||||
if (event.code === 'Space' && event.shiftKey) {
|
||||
distance = distance * -1;
|
||||
}
|
||||
this.window!.scrollBy(0, distance);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setWindow(element) {
|
||||
if (!element)
|
||||
return;
|
||||
this.save();
|
||||
|
||||
if (this.window) {
|
||||
if (this.window.isSameNode(element)) {
|
||||
return;
|
||||
} else {
|
||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||
}
|
||||
}
|
||||
const { averageHeight } = this.props;
|
||||
|
||||
this.window = element;
|
||||
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 2));
|
||||
this.pageDelta = Math.floor(this.pageSize / 4);
|
||||
|
||||
if (this.props.origin === 'bottom') {
|
||||
element.addEventListener('wheel', (event) => {
|
||||
event.preventDefault();
|
||||
const normalized = normalizeWheel(event);
|
||||
element.scrollBy(0, normalized.pixelY * -1);
|
||||
return false;
|
||||
}, { passive: false });
|
||||
|
||||
window.addEventListener('keydown', this.invertedKeyHandler, { passive: false });
|
||||
}
|
||||
this.restore();
|
||||
}
|
||||
|
||||
resetScroll() {
|
||||
if (!this.window) {
|
||||
return;
|
||||
}
|
||||
this.window.scrollTop = 0;
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth = 0;
|
||||
}
|
||||
loadTop = _.throttle(() => this.loadRows(false), 100);
|
||||
loadBottom = _.throttle(() => this.loadRows(true), 100);
|
||||
|
||||
loadRows = async (newer: boolean) => {
|
||||
const dir = newer ? 'bottom' : 'top';
|
||||
if(this.state.loaded[dir]) {
|
||||
return;
|
||||
}
|
||||
log('network', `loading more at ${dir}`);
|
||||
const done = await this.props.loadRows(newer);
|
||||
if(done) {
|
||||
this.setState({
|
||||
loaded: {
|
||||
...this.state.loaded,
|
||||
[dir]: done
|
||||
}
|
||||
});
|
||||
if(newer && this.props.onBottomLoaded) {
|
||||
this.props.onBottomLoaded();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onScroll(event: SyntheticEvent<HTMLElement>) {
|
||||
this.updateScroll();
|
||||
if(!this.window) {
|
||||
// bail if we're going to adjust scroll anyway
|
||||
return;
|
||||
}
|
||||
if(this.saveDepth > 0) {
|
||||
log('bail', 'deep scroll queue');
|
||||
return;
|
||||
}
|
||||
const { onStartReached, onEndReached } = this.props;
|
||||
const windowHeight = this.window.offsetHeight;
|
||||
const { scrollTop, scrollHeight } = this.window;
|
||||
|
||||
const startOffset = this.startOffset();
|
||||
|
||||
if (scrollTop < ZONE_SIZE) {
|
||||
log('scroll', `Entered start zone ${scrollTop}`);
|
||||
if (startOffset === 0) {
|
||||
onStartReached && onStartReached();
|
||||
this.scrollLocked = true;
|
||||
}
|
||||
|
||||
const newOffset =
|
||||
clamp(startOffset - this.pageDelta, 0, this.props.data.size - this.pageSize);
|
||||
if(newOffset < 10) {
|
||||
this.loadBottom();
|
||||
}
|
||||
|
||||
if(newOffset !== startOffset) {
|
||||
this.updateVisible(newOffset);
|
||||
}
|
||||
} else if (scrollTop + windowHeight >= scrollHeight - ZONE_SIZE) {
|
||||
this.scrollLocked = false;
|
||||
log('scroll', `Entered end zone ${scrollTop}`);
|
||||
|
||||
const newOffset =
|
||||
clamp(startOffset + this.pageDelta, 0, this.props.data.size - this.pageSize);
|
||||
if (onEndReached && startOffset === 0) {
|
||||
onEndReached();
|
||||
}
|
||||
|
||||
if((newOffset + (3 * this.pageSize) > this.props.data.size)) {
|
||||
this.loadTop();
|
||||
}
|
||||
|
||||
if(newOffset !== startOffset) {
|
||||
this.updateVisible(newOffset);
|
||||
}
|
||||
} else {
|
||||
this.scrollLocked = false;
|
||||
}
|
||||
}
|
||||
|
||||
restore() {
|
||||
if(!this.window || !this.savedIndex) {
|
||||
return;
|
||||
}
|
||||
if(this.saveDepth !== 1) {
|
||||
log('bail', 'Deep restore');
|
||||
return;
|
||||
}
|
||||
if(this.scrollLocked) {
|
||||
this.resetScroll();
|
||||
requestAnimationFrame(() => {
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth--;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ref = this.childRefs.get(arrToString(this.savedIndex));
|
||||
if(!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newScrollTop = this.props.origin === 'top'
|
||||
? this.savedDistance + ref.offsetTop
|
||||
: this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
||||
|
||||
this.window.scrollTo(0, newScrollTop);
|
||||
requestAnimationFrame(() => {
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth--;
|
||||
});
|
||||
}
|
||||
|
||||
scrollToIndex = (index: BigInteger[]) => {
|
||||
let ref = this.childRefs.get(arrToString(index));
|
||||
if(!ref) {
|
||||
const offset = [...this.props.data].findIndex(([idx]) => indexEqual(idx, index));
|
||||
if(offset === -1) {
|
||||
return;
|
||||
}
|
||||
this.scrollLocked = false;
|
||||
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
||||
requestAnimationFrame(() => {
|
||||
ref = this.childRefs.get(arrToString(index));
|
||||
requestAnimationFrame(() => {
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth = 0;
|
||||
});
|
||||
|
||||
ref?.scrollIntoView({ block: 'center' });
|
||||
});
|
||||
} else {
|
||||
ref?.scrollIntoView({ block: 'center' });
|
||||
requestAnimationFrame(() => {
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth = 0;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
save() {
|
||||
if(!this.window || this.savedIndex) {
|
||||
return;
|
||||
}
|
||||
if(this.saveDepth !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
log('scroll', 'saving...');
|
||||
|
||||
this.saveDepth++;
|
||||
const { visibleItems } = this.state;
|
||||
|
||||
let bottomIndex = visibleItems[visibleItems.length - 1];
|
||||
const { scrollTop, scrollHeight } = this.window;
|
||||
const topSpacing = this.props.origin === 'top' ? scrollTop : scrollHeight - scrollTop;
|
||||
const items = this.props.origin === 'top' ? visibleItems : [...visibleItems].reverse();
|
||||
items.forEach((index) => {
|
||||
const el = this.childRefs.get(arrToString(index));
|
||||
if(!el) {
|
||||
return;
|
||||
}
|
||||
const { offsetTop } = el;
|
||||
if(offsetTop < topSpacing) {
|
||||
bottomIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
if(!bottomIndex) {
|
||||
// weird, shouldn't really happen
|
||||
this.saveDepth--;
|
||||
log('bail', 'no index found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.savedIndex = bottomIndex;
|
||||
const ref = this.childRefs.get(arrToString(bottomIndex))!;
|
||||
if(!ref) {
|
||||
this.saveDepth--;
|
||||
log('bail', 'missing ref');
|
||||
return;
|
||||
}
|
||||
const { offsetTop } = ref;
|
||||
this.savedDistance = topSpacing - offsetTop;
|
||||
}
|
||||
|
||||
// disabled until we work out race conditions with loading new nodes
|
||||
shiftLayout = { save: () => {}, restore: () => {} };
|
||||
|
||||
setRef = (element: HTMLElement | null, index: BigInteger[]) => {
|
||||
if(element) {
|
||||
this.childRefs.set(arrToString(index), element);
|
||||
this.orphans.delete(arrToString(index));
|
||||
} else {
|
||||
this.orphans.add(arrToString(index));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
visibleItems
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
origin = 'top',
|
||||
renderer,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
const isTop = origin === 'top';
|
||||
|
||||
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||
const children = isTop ? visibleItems : [...visibleItems].reverse();
|
||||
|
||||
const atStart =
|
||||
indexEqual(
|
||||
(this.props.data.peekLargest()?.[0] ?? [bigInt.zero]),
|
||||
(visibleItems?.[0] || [bigInt.zero])
|
||||
);
|
||||
|
||||
const atEnd =
|
||||
indexEqual(
|
||||
(this.props.data.peekSmallest()?.[0] ?? [bigInt.zero]),
|
||||
(visibleItems?.[visibleItems.length - 1] || [bigInt.zero])
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!IS_IOS && (<Box borderRadius={3} top ={isTop ? '0' : undefined}
|
||||
bottom={!isTop ? '0' : undefined} ref={(el) => {
|
||||
this.scrollRef = el;
|
||||
}}
|
||||
right={0} height="50px"
|
||||
position="absolute" width="4px"
|
||||
backgroundColor="lightGray"
|
||||
/>)}
|
||||
|
||||
<ScrollbarLessBox overflowY='scroll' ref={this.setWindow} onScroll={this.onScroll} style={{ ...style, ...{ transform }, 'WebkitOverflowScrolling': 'auto' }}>
|
||||
<Box style={{ transform, width: 'calc(100% - 4px)' }}>
|
||||
{(isTop ? !atStart : !atEnd) && (
|
||||
<Center height={5}>
|
||||
<LoadingSpinner />
|
||||
</Center>
|
||||
)}
|
||||
<VirtualContext.Provider value={this.shiftLayout}>
|
||||
{children.map(index => (
|
||||
<VirtualChild
|
||||
key={arrToString(index)}
|
||||
setRef={this.setRef}
|
||||
index={index}
|
||||
scrollWindow={this.window}
|
||||
renderer={renderer}
|
||||
/>
|
||||
))}
|
||||
</VirtualContext.Provider>
|
||||
{(!isTop ? !atStart : !atEnd) &&
|
||||
(<Center height={5}>
|
||||
<LoadingSpinner />
|
||||
</Center>)}
|
||||
</Box>
|
||||
</ScrollbarLessBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface VirtualChildProps {
|
||||
index: BigInteger[];
|
||||
scrollWindow: any;
|
||||
setRef: (el: HTMLElement | null, index: BigInteger[]) => void;
|
||||
renderer: (p: RendererProps) => JSX.Element | null;
|
||||
}
|
||||
|
||||
function VirtualChild(props: VirtualChildProps) {
|
||||
const { setRef, renderer: Renderer, ...rest } = props;
|
||||
|
||||
const ref = useCallback((el: HTMLElement | null) => {
|
||||
setRef(el, props.index);
|
||||
// VirtualChild should always be keyed on the index, so the index should be
|
||||
// valid for the entire lifecycle of the component, hence no dependencies
|
||||
}, []);
|
||||
|
||||
return <Renderer ref={ref} {...rest} />;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export function AudioPlayer(props: { url: string; title?: string }) {
|
||||
} else {
|
||||
ref.current.play();
|
||||
}
|
||||
setPlaying((p) => !p);
|
||||
setPlaying(p => !p);
|
||||
},
|
||||
[ref, playing]
|
||||
);
|
||||
|
27
pkg/interface/src/views/components/BlockScroller.tsx
Normal file
27
pkg/interface/src/views/components/BlockScroller.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { GraphNode } from '@urbit/api';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import React from 'react';
|
||||
import VirtualScroller, { VirtualScrollerProps } from './VirtualScroller';
|
||||
|
||||
type BlockScrollerProps = Omit<
|
||||
VirtualScrollerProps<BigInteger, [BigInteger, GraphNode][]>,
|
||||
'keyEq' | 'keyToString' | 'keyBunt'
|
||||
>;
|
||||
|
||||
const keyEq = (a: BigInteger, b: BigInteger) => a.eq(b);
|
||||
const keyToString = (a: BigInteger) => a.toString();
|
||||
|
||||
export const BlockScroller = React.forwardRef<
|
||||
VirtualScroller<BigInteger, [BigInteger, GraphNode][]>,
|
||||
BlockScrollerProps
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<VirtualScroller<BigInteger, [BigInteger, GraphNode][]>
|
||||
ref={ref}
|
||||
{...props}
|
||||
keyEq={keyEq}
|
||||
keyToString={keyToString}
|
||||
keyBunt={bigInt.zero}
|
||||
/>
|
||||
);
|
||||
});
|
27
pkg/interface/src/views/components/GraphScroller.tsx
Normal file
27
pkg/interface/src/views/components/GraphScroller.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { GraphNode } from '@urbit/api';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import React from 'react';
|
||||
import VirtualScroller, { VirtualScrollerProps } from './VirtualScroller';
|
||||
|
||||
type GraphScrollerProps = Omit<
|
||||
VirtualScrollerProps<BigInteger, GraphNode>,
|
||||
'keyEq' | 'keyToString' | 'keyBunt'
|
||||
>;
|
||||
|
||||
const keyEq = (a: BigInteger, b: BigInteger) => a.eq(b);
|
||||
const keyToString = (a: BigInteger) => a.toString();
|
||||
|
||||
export const GraphScroller = React.forwardRef<
|
||||
VirtualScroller<BigInteger, GraphNode>,
|
||||
GraphScrollerProps
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<VirtualScroller
|
||||
ref={ref}
|
||||
{...props}
|
||||
keyEq={keyEq}
|
||||
keyToString={keyToString}
|
||||
keyBunt={bigInt.zero}
|
||||
/>
|
||||
);
|
||||
});
|
48
pkg/interface/src/views/components/ThreadScroller.tsx
Normal file
48
pkg/interface/src/views/components/ThreadScroller.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { BigInteger } from 'big-integer';
|
||||
import React from 'react';
|
||||
import VirtualScroller, { VirtualScrollerProps } from './VirtualScroller';
|
||||
|
||||
import { arrToString } from '@urbit/api/lib/BigIntArrayOrderedMap';
|
||||
import { FlatGraphNode } from '@urbit/api';
|
||||
|
||||
type ThreadScrollerProps = Omit<
|
||||
VirtualScrollerProps<BigInteger[], FlatGraphNode>,
|
||||
'keyEq' | 'keyToString' | 'keyBunt'
|
||||
>;
|
||||
|
||||
export function keyEq(a: BigInteger[], b: BigInteger[]) {
|
||||
const aLen = a.length;
|
||||
const bLen = b.length;
|
||||
|
||||
if (aLen === bLen) {
|
||||
let i = 0;
|
||||
while (i < aLen && i < bLen) {
|
||||
if (a[i].eq(b[i])) {
|
||||
if (i === aLen - 1) {
|
||||
return true;
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
const keyBunt = [];
|
||||
|
||||
export const ThreadScroller = React.forwardRef<
|
||||
VirtualScroller<BigInteger[], FlatGraphNode>,
|
||||
ThreadScrollerProps
|
||||
>((props: ThreadScrollerProps, ref) => {
|
||||
return (
|
||||
<VirtualScroller<BigInteger[], FlatGraphNode>
|
||||
ref={ref}
|
||||
{...props}
|
||||
keyEq={keyEq}
|
||||
keyToString={arrToString}
|
||||
keyBunt={keyBunt}
|
||||
/>
|
||||
);
|
||||
});
|
@ -1,6 +1,4 @@
|
||||
import { Box, Center, LoadingSpinner } from '@tlon/indigo-react';
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import _ from 'lodash';
|
||||
import normalizeWheel from 'normalize-wheel';
|
||||
import React, { Component, SyntheticEvent, useCallback } from 'react';
|
||||
@ -17,13 +15,20 @@ const ScrollbarLessBox = styled(Box)`
|
||||
}
|
||||
`;
|
||||
|
||||
interface RendererProps {
|
||||
index: BigInteger;
|
||||
interface RendererProps<K> {
|
||||
index: K;
|
||||
scrollWindow: any;
|
||||
ref: (el: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
interface VirtualScrollerProps<T> {
|
||||
interface OrderedMap<K,V> extends Iterable<[K,V]> {
|
||||
peekLargest: () => [K,V] | undefined;
|
||||
peekSmallest: () => [K,V] | undefined;
|
||||
size: number;
|
||||
keys: () => K[];
|
||||
}
|
||||
|
||||
export interface VirtualScrollerProps<K,V> {
|
||||
/**
|
||||
* Start scroll from
|
||||
*/
|
||||
@ -37,7 +42,7 @@ interface VirtualScrollerProps<T> {
|
||||
/**
|
||||
* The data to iterate over
|
||||
*/
|
||||
data: BigIntOrderedMap<T>;
|
||||
data: OrderedMap<K,V>;
|
||||
/*
|
||||
* The component to render the items
|
||||
*
|
||||
@ -46,12 +51,11 @@ interface VirtualScrollerProps<T> {
|
||||
* This component must be referentially stable, so either use `useCallback` or
|
||||
* a instance method. It must also forward the DOM ref from its root DOM node
|
||||
*/
|
||||
renderer: (props: RendererProps) => JSX.Element | null;
|
||||
renderer: (props: RendererProps<K>) => JSX.Element | null;
|
||||
onStartReached?(): void;
|
||||
onEndReached?(): void;
|
||||
size: number;
|
||||
pendingSize: number;
|
||||
totalSize?: number;
|
||||
/*
|
||||
* Average height of a single rendered item
|
||||
*
|
||||
@ -73,10 +77,22 @@ interface VirtualScrollerProps<T> {
|
||||
* Callback to execute when finished loading from start
|
||||
*/
|
||||
onBottomLoaded?: () => void;
|
||||
/*
|
||||
* equality function for the key type
|
||||
*/
|
||||
keyEq: (a: K, b: K) => boolean;
|
||||
/*
|
||||
* string conversion for key type
|
||||
*/
|
||||
keyToString: (k: K) => string;
|
||||
/*
|
||||
* default value for key type
|
||||
*/
|
||||
keyBunt: K;
|
||||
}
|
||||
|
||||
interface VirtualScrollerState {
|
||||
visibleItems: BigInteger[];
|
||||
interface VirtualScrollerState<K> {
|
||||
visibleItems: K[];
|
||||
scrollbar: number;
|
||||
loaded: {
|
||||
top: boolean;
|
||||
@ -85,7 +101,9 @@ interface VirtualScrollerState {
|
||||
}
|
||||
|
||||
type LogLevel = 'scroll' | 'network' | 'bail' | 'reflow';
|
||||
const logLevel = ['network', 'bail', 'scroll', 'reflow'] as LogLevel[];
|
||||
const logLevel = process.env.NODE_ENV === 'production'
|
||||
? []
|
||||
: ['network', 'bail', 'scroll', 'reflow'] as LogLevel[];
|
||||
|
||||
const log = (level: LogLevel, message: string) => {
|
||||
if(logLevel.includes(level)) {
|
||||
@ -104,7 +122,7 @@ const ZONE_SIZE = IS_IOS ? 20 : 80;
|
||||
* VirtualScroller does not clean up or reset itself, so please use `key`
|
||||
* to ensure a new instance is created for each BigIntOrderedMap
|
||||
*/
|
||||
export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T>, VirtualScrollerState> {
|
||||
export default class VirtualScroller<K,V> extends Component<VirtualScrollerProps<K,V>, VirtualScrollerState<K>> {
|
||||
/*
|
||||
* A reference to our scroll container
|
||||
*/
|
||||
@ -120,7 +138,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
/*
|
||||
* If saving, the bottommost visible element that we pin our scroll to
|
||||
*/
|
||||
private savedIndex: BigInteger | null = null;
|
||||
private savedIndex: K | null = null;
|
||||
/*
|
||||
* If saving, the distance between the top of `this.savedEl` and the bottom
|
||||
* of the screen
|
||||
@ -144,7 +162,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
private cleanupRefInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(props: VirtualScrollerProps<T>) {
|
||||
constructor(props: VirtualScrollerProps<K,V>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visibleItems: [],
|
||||
@ -177,8 +195,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
return;
|
||||
}
|
||||
[...this.orphans].forEach((o) => {
|
||||
const index = bigInt(o);
|
||||
this.childRefs.delete(index.toString());
|
||||
this.childRefs.delete(o);
|
||||
});
|
||||
this.orphans.clear();
|
||||
};
|
||||
@ -198,7 +215,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
this.scrollRef.style[this.props.origin] = `${result}px`;
|
||||
}, 50);
|
||||
|
||||
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState) {
|
||||
componentDidUpdate(prevProps: VirtualScrollerProps<K,V>, _prevState: VirtualScrollerState<K>) {
|
||||
const { size, pendingSize } = this.props;
|
||||
|
||||
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
||||
@ -220,13 +237,13 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
|
||||
startOffset() {
|
||||
const { data } = this.props;
|
||||
const { data, keyEq } = this.props;
|
||||
const startIndex = this.state.visibleItems?.[0];
|
||||
if(!startIndex) {
|
||||
return 0;
|
||||
}
|
||||
const dataList = Array.from(data);
|
||||
const offset = dataList.findIndex(([i]) => i.eq(startIndex));
|
||||
const offset = dataList.findIndex(([i]) => keyEq(i, startIndex));
|
||||
if(offset === -1) {
|
||||
// TODO: revisit when we remove nodes for any other reason than
|
||||
// pending indices being removed
|
||||
@ -401,6 +418,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
|
||||
restore() {
|
||||
const { keyToString } = this.props;
|
||||
if(!this.window || !this.savedIndex) {
|
||||
return;
|
||||
}
|
||||
@ -418,7 +436,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
return;
|
||||
}
|
||||
|
||||
const ref = this.childRefs.get(this.savedIndex.toString());
|
||||
const ref = this.childRefs.get(keyToString(this.savedIndex));
|
||||
if(!ref) {
|
||||
return;
|
||||
}
|
||||
@ -435,17 +453,18 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
});
|
||||
}
|
||||
|
||||
scrollToIndex = (index: BigInteger) => {
|
||||
let ref = this.childRefs.get(index.toString());
|
||||
scrollToIndex = (index: K) => {
|
||||
const { keyToString, keyEq } = this.props;
|
||||
let ref = this.childRefs.get(keyToString(index));
|
||||
if(!ref) {
|
||||
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
||||
const offset = [...this.props.data].findIndex(([idx]) => keyEq(idx, index));
|
||||
if(offset === -1) {
|
||||
return;
|
||||
}
|
||||
this.scrollLocked = false;
|
||||
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
||||
requestAnimationFrame(() => {
|
||||
ref = this.childRefs.get(index.toString());
|
||||
ref = this.childRefs.get(keyToString(index));
|
||||
requestAnimationFrame(() => {
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
@ -468,6 +487,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
if(!this.window || this.savedIndex) {
|
||||
return;
|
||||
}
|
||||
log('reflow', `saving @ ${this.saveDepth}`);
|
||||
if(this.saveDepth !== 0) {
|
||||
return;
|
||||
}
|
||||
@ -476,13 +496,14 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
this.saveDepth++;
|
||||
const { visibleItems } = this.state;
|
||||
const { keyToString } = this.props;
|
||||
|
||||
let bottomIndex = visibleItems[visibleItems.length - 1];
|
||||
const { scrollTop, scrollHeight } = this.window;
|
||||
const topSpacing = this.props.origin === 'top' ? scrollTop : scrollHeight - scrollTop;
|
||||
const items = this.props.origin === 'top' ? visibleItems : [...visibleItems].reverse();
|
||||
items.forEach((index) => {
|
||||
const el = this.childRefs.get(index.toString());
|
||||
const el = this.childRefs.get(keyToString(index));
|
||||
if(!el) {
|
||||
return;
|
||||
}
|
||||
@ -500,7 +521,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
|
||||
this.savedIndex = bottomIndex;
|
||||
const ref = this.childRefs.get(bottomIndex.toString())!;
|
||||
const ref = this.childRefs.get(keyToString(bottomIndex))!;
|
||||
if(!ref) {
|
||||
this.saveDepth--;
|
||||
log('bail', 'missing ref');
|
||||
@ -513,12 +534,13 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
// disabled until we work out race conditions with loading new nodes
|
||||
shiftLayout = { save: () => {}, restore: () => {} };
|
||||
|
||||
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
||||
setRef = (element: HTMLElement | null, index: K) => {
|
||||
const { keyToString } = this.props;
|
||||
if(element) {
|
||||
this.childRefs.set(index.toString(), element);
|
||||
this.orphans.delete(index.toString());
|
||||
this.childRefs.set(keyToString(index), element);
|
||||
this.orphans.delete(keyToString(index));
|
||||
} else {
|
||||
this.orphans.add(index.toString());
|
||||
this.orphans.add(keyToString(index));
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,7 +552,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
const {
|
||||
origin = 'top',
|
||||
renderer,
|
||||
style
|
||||
style,
|
||||
keyEq,
|
||||
keyBunt,
|
||||
keyToString
|
||||
} = this.props;
|
||||
|
||||
const isTop = origin === 'top';
|
||||
@ -538,8 +563,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||
const children = isTop ? visibleItems : [...visibleItems].reverse();
|
||||
|
||||
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems?.[0] || bigInt.zero);
|
||||
const atEnd = (this.props.data.peekSmallest()?.[0] ?? bigInt.zero).eq(visibleItems?.[visibleItems.length -1 ] || bigInt.zero);
|
||||
const atStart = keyEq(this.props.data.peekLargest()?.[0] ?? keyBunt, visibleItems?.[0] || keyBunt);
|
||||
const atEnd = keyEq(this.props.data.peekSmallest()?.[0] ?? keyBunt, visibleItems?.[visibleItems.length -1 ] || keyBunt);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -559,8 +584,8 @@ backgroundColor="lightGray"
|
||||
</Center>)}
|
||||
<VirtualContext.Provider value={this.shiftLayout}>
|
||||
{children.map(index => (
|
||||
<VirtualChild
|
||||
key={index.toString()}
|
||||
<VirtualChild<K>
|
||||
key={keyToString(index)}
|
||||
setRef={this.setRef}
|
||||
index={index}
|
||||
scrollWindow={this.window}
|
||||
@ -579,14 +604,14 @@ backgroundColor="lightGray"
|
||||
}
|
||||
}
|
||||
|
||||
interface VirtualChildProps {
|
||||
index: BigInteger;
|
||||
interface VirtualChildProps<K> {
|
||||
index: K;
|
||||
scrollWindow: any;
|
||||
setRef: (el: HTMLElement | null, index: BigInteger) => void;
|
||||
renderer: (p: RendererProps) => JSX.Element | null;
|
||||
setRef: (el: HTMLElement | null, index: K) => void;
|
||||
renderer: (p: RendererProps<K>) => JSX.Element | null;
|
||||
}
|
||||
|
||||
function VirtualChild(props: VirtualChildProps) {
|
||||
function VirtualChild<K>(props: VirtualChildProps<K>) {
|
||||
const { setRef, renderer: Renderer, ...rest } = props;
|
||||
|
||||
const ref = useCallback((el: HTMLElement | null) => {
|
||||
|
@ -5,9 +5,9 @@ import bigInt from 'big-integer';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import VirtualScroller from '~/views/components/VirtualScroller';
|
||||
import PostItem from './PostItem/PostItem';
|
||||
import PostInput from './PostInput';
|
||||
import { GraphScroller } from '~/views/components/GraphScroller';
|
||||
import useGraphState, { GraphState } from '~/logic/state/graph';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
@ -210,14 +210,13 @@ class PostFeed extends React.Component<PostFeedProps, any> {
|
||||
|
||||
return (
|
||||
<Col width="100%" height="100%" position="relative">
|
||||
<VirtualScroller
|
||||
<GraphScroller
|
||||
key={history.location.pathname}
|
||||
origin="top"
|
||||
offset={0}
|
||||
data={graph}
|
||||
averageHeight={106}
|
||||
averageHeight={80}
|
||||
size={graph.size}
|
||||
totalSize={graph.size}
|
||||
style={virtualScrollerStyle}
|
||||
pendingSize={pendingSize}
|
||||
renderer={this.renderItem}
|
||||
|
@ -4,10 +4,10 @@ import bigInt from 'big-integer';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, useHistory } from 'react-router';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import ArrayVirtualScroller, {
|
||||
indexEqual,
|
||||
import {
|
||||
arrToString
|
||||
} from '~/views/components/ArrayVirtualScroller';
|
||||
} from '@urbit/api/lib/BigIntArrayOrderedMap';
|
||||
import { keyEq, ThreadScroller } from '~/views/components/ThreadScroller';
|
||||
import PostItem from './PostItem/PostItem';
|
||||
import PostInput from './PostInput';
|
||||
import useGraphState, { GraphState } from '~/logic/state/graph';
|
||||
@ -66,9 +66,9 @@ class PostFlatFeed extends React.Component<PostFeedProps, {}> {
|
||||
|
||||
const first = flatGraph.peekLargest()?.[0];
|
||||
const last = flatGraph.peekSmallest()?.[0];
|
||||
const isLast = last ? indexEqual(index, last) : false;
|
||||
const isLast = last ? keyEq(index, last) : false;
|
||||
|
||||
if (indexEqual(index, (first ?? [bigInt.zero]))) {
|
||||
if (keyEq(index, (first ?? [bigInt.zero]))) {
|
||||
if (isThread) {
|
||||
return (
|
||||
<Col
|
||||
@ -195,12 +195,12 @@ class PostFlatFeed extends React.Component<PostFeedProps, {}> {
|
||||
|
||||
return (
|
||||
<Col width="100%" height="100%" position="relative">
|
||||
<ArrayVirtualScroller
|
||||
<ThreadScroller
|
||||
key={history.location.pathname}
|
||||
origin="top"
|
||||
offset={0}
|
||||
data={flatGraph}
|
||||
averageHeight={122}
|
||||
averageHeight={80}
|
||||
size={flatGraph.size}
|
||||
style={virtualScrollerStyle}
|
||||
pendingSize={pendingSize}
|
||||
|
@ -6,7 +6,7 @@ import React, {
|
||||
} from 'react';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import { arrToString } from '~/views/components/ArrayVirtualScroller';
|
||||
import { arrToString } from '@urbit/api/lib/BigIntArrayOrderedMap';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import PostFlatFeed from './PostFlatFeed';
|
||||
import PostInput from './PostInput';
|
||||
|
@ -13,7 +13,7 @@ export const createBlankNodeWithChildPost = (
|
||||
parentIndex: string = '',
|
||||
childIndex: string = '',
|
||||
contents: Content[]
|
||||
): GraphNodePoke => {
|
||||
): GraphNodePoke => {
|
||||
const date = unixToDa(Date.now()).toString();
|
||||
const nodeIndex = parentIndex + '/' + date;
|
||||
|
||||
@ -40,7 +40,7 @@ export const createBlankNodeWithChildPost = (
|
||||
signatures: []
|
||||
},
|
||||
children: childGraph
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const markPending = (nodes: any): any => {
|
||||
@ -310,7 +310,7 @@ export const setScreen = (screen: boolean): Poke<any> => dmAction({ screen });
|
||||
*
|
||||
* @param ship the ship to accept
|
||||
*/
|
||||
export const acceptDm = (ship: string) => dmAction({
|
||||
export const acceptDm = (ship: string) => dmAction({
|
||||
accept: ship
|
||||
});
|
||||
|
||||
@ -319,7 +319,7 @@ export const acceptDm = (ship: string) => dmAction({
|
||||
*
|
||||
* @param ship the ship to accept
|
||||
*/
|
||||
export const declineDm = (ship: string) => dmAction({
|
||||
export const declineDm = (ship: string) => dmAction({
|
||||
decline: ship
|
||||
});
|
||||
|
||||
@ -368,7 +368,7 @@ export const addDmMessage = (our: PatpNoSig, ship: Patp, contents: Content[]): P
|
||||
|
||||
const encodeIndex = (idx: string) => idx.split('/').map(decToUd).join('/');
|
||||
|
||||
/**
|
||||
/**
|
||||
* Fetch newest (larger keys) nodes in a graph under some index
|
||||
*
|
||||
* @param ship ship of graph
|
||||
@ -386,8 +386,8 @@ export const getNewest = (
|
||||
path: `/newest/${ship}/${name}/${count}${encodeIndex(index)}`
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch nodes in a graph that are older (key is smaller) and direct
|
||||
/**
|
||||
* Fetch nodes in a graph that are older (key is smaller) and direct
|
||||
* siblings of some index
|
||||
*
|
||||
* @param ship ship of graph
|
||||
@ -405,8 +405,8 @@ export const getOlderSiblings = (
|
||||
path: `/node-siblings/older/${ship}/${name}/${count}${encodeIndex(index)}`
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch nodes in a graph that are younger (key is larger) and direct
|
||||
/**
|
||||
* Fetch nodes in a graph that are younger (key is larger) and direct
|
||||
* siblings of some index
|
||||
*
|
||||
* @param ship ship of graph
|
||||
@ -437,8 +437,8 @@ export const getShallowChildren = (ship: string, name: string, index = '') => ({
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Fetch newest nodes in a graph as a flat map, including children,
|
||||
/**
|
||||
* Fetch newest nodes in a graph as a flat map, including children,
|
||||
* optionally starting at a specified key
|
||||
*
|
||||
* @param ship ship of graph
|
||||
@ -472,7 +472,7 @@ export const getFirstborn = (
|
||||
index: string
|
||||
): Scry => ({
|
||||
app: 'graph-store',
|
||||
path: `/firstborn/${ship}/${name}/${encodeIndex(index)}`
|
||||
path: `/firstborn/${ship}/${name}${encodeIndex(index)}`
|
||||
});
|
||||
|
||||
/**
|
||||
@ -489,7 +489,7 @@ export const getNode = (
|
||||
index: string
|
||||
): Scry => ({
|
||||
app: 'graph-store',
|
||||
path: `/node/${ship}/${name}/${encodeIndex(index)}`
|
||||
path: `/node/${ship}/${name}${encodeIndex(index)}`
|
||||
});
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user