mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-14 15:04:14 +03:00
interface: flat timeline first pass
This commit is contained in:
parent
dbb58bd00d
commit
1e20f52050
@ -348,7 +348,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDeepNewest(ship: string, resource: string, startTime = null, count: number) {
|
async getDeepNewest(ship: string, resource: string, startTime = null, count: number) {
|
||||||
const start = startTime ? decToUd(startTime) : null;
|
const start = startTime ? decToUd(startTime) : 'null';
|
||||||
const data = await this.scry<any>('graph-store',
|
const data = await this.scry<any>('graph-store',
|
||||||
`/deep-nodes-up-to/${ship}/${resource}/${count}/${start}`
|
`/deep-nodes-up-to/${ship}/${resource}/${count}/${start}`
|
||||||
);
|
);
|
||||||
|
@ -26,9 +26,8 @@ export const GraphReducer = (json) => {
|
|||||||
|
|
||||||
const flat = _.get(json, 'graph-update-flat', false);
|
const flat = _.get(json, 'graph-update-flat', false);
|
||||||
if (flat) {
|
if (flat) {
|
||||||
reduceState<GraphState, any>(useGraphState, loose, [addNodesFlat]);
|
reduceState<GraphState, any>(useGraphState, flat, [addNodesFlat]);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addNodesLoose = (json: any, state: GraphState): GraphState => {
|
const addNodesLoose = (json: any, state: GraphState): GraphState => {
|
||||||
@ -60,7 +59,7 @@ const addNodesFlat = (json: any, state: GraphState): GraphState => {
|
|||||||
const indices = Array.from(Object.keys(data.nodes));
|
const indices = Array.from(Object.keys(data.nodes));
|
||||||
|
|
||||||
indices.forEach((index) => {
|
indices.forEach((index) => {
|
||||||
const node = data.nodes[index];
|
let node = data.nodes[index];
|
||||||
if (index.split('/').length === 0) {
|
if (index.split('/').length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -73,14 +72,11 @@ const addNodesFlat = (json: any, state: GraphState): GraphState => {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node.children = mapifyChildren({});
|
||||||
state.flatGraphs[resource] =
|
state.flatGraphs[resource] =
|
||||||
state.flatGraphs[resource].set(
|
state.flatGraphs[resource].set(indexArr, node);
|
||||||
indexArr,
|
|
||||||
produce(node, (draft) => {
|
|
||||||
draft.children = mapifyChildren({});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
@ -120,8 +116,13 @@ const addGraph = (json, state: GraphState): GraphState => {
|
|||||||
state.graphs = {};
|
state.graphs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!('flatGraphs' in state)) {
|
||||||
|
state.flatGraphs = {};
|
||||||
|
}
|
||||||
|
|
||||||
const resource = data.resource.ship + '/' + data.resource.name;
|
const resource = data.resource.ship + '/' + data.resource.name;
|
||||||
state.graphs[resource] = new BigIntOrderedMap();
|
state.graphs[resource] = new BigIntOrderedMap();
|
||||||
|
state.flatGraphs[resource] = new BigIntArrayOrderedMap();
|
||||||
state.graphTimesentMap[resource] = {};
|
state.graphTimesentMap[resource] = {};
|
||||||
|
|
||||||
state.graphs[resource] = state.graphs[resource].gas(Object.keys(data.graph).map((idx) => {
|
state.graphs[resource] = state.graphs[resource].gas(Object.keys(data.graph).map((idx) => {
|
||||||
@ -139,6 +140,10 @@ const removeGraph = (json, state: GraphState): GraphState => {
|
|||||||
if (!('graphs' in state)) {
|
if (!('graphs' in state)) {
|
||||||
state.graphs = {};
|
state.graphs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!('graphs' in state)) {
|
||||||
|
state.flatGraphs = {};
|
||||||
|
}
|
||||||
const resource = data.ship + '/' + data.name;
|
const resource = data.ship + '/' + data.name;
|
||||||
state.graphKeys.delete(resource);
|
state.graphKeys.delete(resource);
|
||||||
delete state.graphs[resource];
|
delete state.graphs[resource];
|
||||||
@ -219,14 +224,18 @@ const addNodes = (json, state) => {
|
|||||||
const data = _.get(json, 'add-nodes', false);
|
const data = _.get(json, 'add-nodes', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!('graphs' in state)) {
|
if (!('graphs' in state)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = data.resource.ship + '/' + data.resource.name;
|
const resource = data.resource.ship + '/' + data.resource.name;
|
||||||
if (!(resource in state.graphs)) {
|
if (!(resource in state.graphs)) {
|
||||||
state.graphs[resource] = new BigIntOrderedMap();
|
state.graphs[resource] = new BigIntOrderedMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(resource in state.flatGraphs)) {
|
||||||
|
state.flatGraphs[resource] = new BigIntArrayOrderedMap();
|
||||||
|
}
|
||||||
|
|
||||||
if (!(resource in state.graphTimesentMap)) {
|
if (!(resource in state.graphTimesentMap)) {
|
||||||
state.graphTimesentMap[resource] = {};
|
state.graphTimesentMap[resource] = {};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Association, deSig, GraphNode, Graphs, resourceFromPath } from '@urbit/api';
|
import { Association, deSig, GraphNode, Graphs, FlatGraphs, resourceFromPath } from '@urbit/api';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { BaseState, createState } from './base';
|
import { BaseState, createState } from './base';
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ export interface GraphState extends BaseState<GraphState> {
|
|||||||
|
|
||||||
const useGraphState = createState<GraphState>('Graph', {
|
const useGraphState = createState<GraphState>('Graph', {
|
||||||
graphs: {},
|
graphs: {},
|
||||||
|
flatGraphs: {},
|
||||||
graphKeys: new Set(),
|
graphKeys: new Set(),
|
||||||
looseNodes: {},
|
looseNodes: {},
|
||||||
pendingIndices: {},
|
pendingIndices: {},
|
||||||
@ -130,7 +131,7 @@ const useGraphState = createState<GraphState>('Graph', {
|
|||||||
// });
|
// });
|
||||||
// graphReducer(node);
|
// graphReducer(node);
|
||||||
// },
|
// },
|
||||||
}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap']);
|
}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap', 'flatGraphs']);
|
||||||
|
|
||||||
export function useGraph(ship: string, name: string) {
|
export function useGraph(ship: string, name: string) {
|
||||||
return useGraphState(
|
return useGraphState(
|
||||||
|
635
pkg/interface/src/views/components/ArrayVirtualScroller.tsx
Normal file
635
pkg/interface/src/views/components/ArrayVirtualScroller.tsx
Normal file
@ -0,0 +1,635 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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[]) {
|
||||||
|
let aLen = a.length;
|
||||||
|
let 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;
|
||||||
|
totalSize: 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<T> {
|
||||||
|
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<T>> {
|
||||||
|
/**
|
||||||
|
* 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, offsetHeight } = this.window;
|
||||||
|
|
||||||
|
const unloaded = (this.startOffset() / this.pageSize);
|
||||||
|
const totalpages = this.props.size / this.pageSize;
|
||||||
|
|
||||||
|
const loaded = (scrollTop / scrollHeight);
|
||||||
|
const total = unloaded + loaded;
|
||||||
|
const result = ((unloaded + loaded) / totalpages) * this.window.offsetHeight;
|
||||||
|
this.scrollRef.style[this.props.origin] = `${result}px`;
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
||||||
|
const { size, data, offset, 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
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
const scrollEnd = scrollTop + windowHeight;
|
||||||
|
|
||||||
|
if (scrollTop < ZONE_SIZE) {
|
||||||
|
log('scroll', `Entered start zone ${scrollTop}`);
|
||||||
|
if (startOffset === 0) {
|
||||||
|
onStartReached && onStartReached();
|
||||||
|
this.scrollLocked = true;
|
||||||
|
}
|
||||||
|
const newOffset = Math.max(0, startOffset - this.pageDelta);
|
||||||
|
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 = Math.min(startOffset + this.pageDelta, 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} />;
|
||||||
|
}
|
||||||
|
|
@ -414,8 +414,9 @@ export function Graphdown<T extends {} = {}>(
|
|||||||
{...nodeRest}
|
{...nodeRest}
|
||||||
tall={tall}
|
tall={tall}
|
||||||
>
|
>
|
||||||
{children.map((c) => (
|
{children.map((c, idx) => (
|
||||||
<Graphdown
|
<Graphdown
|
||||||
|
key={idx}
|
||||||
transcluded={transcluded}
|
transcluded={transcluded}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
@ -0,0 +1,120 @@
|
|||||||
|
import { Col } from '@tlon/indigo-react';
|
||||||
|
import React, {
|
||||||
|
useEffect
|
||||||
|
} from 'react';
|
||||||
|
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||||
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import { Loading } from '~/views/components/Loading';
|
||||||
|
import { GroupFeedHeader } from './GroupFeedHeader';
|
||||||
|
import PostThread from './Post/PostThread';
|
||||||
|
import PostFlatTimeline from './Post/PostFlatTimeline';
|
||||||
|
|
||||||
|
function GroupFlatFeed(props) {
|
||||||
|
const {
|
||||||
|
baseUrl,
|
||||||
|
api,
|
||||||
|
graphPath,
|
||||||
|
groupPath,
|
||||||
|
vip
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const groups = useGroupState(state => state.groups);
|
||||||
|
const group = groups[groupPath];
|
||||||
|
|
||||||
|
const associations = useMetadataState(state => state.associations);
|
||||||
|
const flatGraphs = useGraphState(state => state.flatGraphs);
|
||||||
|
|
||||||
|
const graphResource =
|
||||||
|
graphPath ? resourceFromPath(graphPath) : resourceFromPath('/ship/~zod/null');
|
||||||
|
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
||||||
|
|
||||||
|
const pendingSize = Object.keys(
|
||||||
|
graphTimesentMap[`${graphResource.ship.slice(1)}/${graphResource.name}`] ||
|
||||||
|
{}
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const relativePath = path => baseUrl + path;
|
||||||
|
const association = associations.graph[graphPath];
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
const locationUrl = history.location.pathname;
|
||||||
|
|
||||||
|
const graphId = `${graphResource.ship.slice(1)}/${graphResource.name}`;
|
||||||
|
const flatGraph = flatGraphs[graphId];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: VirtualScroller should support lower starting values than 100
|
||||||
|
if (graphResource.ship === '~zod' && graphResource.name === 'null') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api.graph.getDeepNewest(graphResource.ship, graphResource.name, null, 100);
|
||||||
|
api.hark.markCountAsRead(association, '/', 'post');
|
||||||
|
}, [graphPath]);
|
||||||
|
|
||||||
|
if (!graphPath) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
display="flex"
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<GroupFeedHeader
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
graphs={flatGraphs}
|
||||||
|
vip={vip}
|
||||||
|
graphResource={graphResource}
|
||||||
|
/>
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={[relativePath('/'), relativePath('/feed')]}
|
||||||
|
render={(routeProps) => {
|
||||||
|
return (
|
||||||
|
<PostFlatTimeline
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
api={api}
|
||||||
|
history={history}
|
||||||
|
graphPath={graphPath}
|
||||||
|
group={group}
|
||||||
|
association={association}
|
||||||
|
vip={vip}
|
||||||
|
flatGraph={flatGraph}
|
||||||
|
pendingSize={pendingSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={relativePath('/feed/:index+')}
|
||||||
|
render={(routeProps) => {
|
||||||
|
return (
|
||||||
|
<PostThread
|
||||||
|
locationUrl={locationUrl}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
api={api}
|
||||||
|
history={history}
|
||||||
|
graphPath={graphPath}
|
||||||
|
group={group}
|
||||||
|
association={association}
|
||||||
|
vip={vip}
|
||||||
|
pendingSize={pendingSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { GroupFlatFeed };
|
@ -8,6 +8,8 @@ import { AddFeedBanner } from './AddFeedBanner';
|
|||||||
import { EmptyGroupHome } from './EmptyGroupHome';
|
import { EmptyGroupHome } from './EmptyGroupHome';
|
||||||
import { EnableGroupFeed } from './EnableGroupFeed';
|
import { EnableGroupFeed } from './EnableGroupFeed';
|
||||||
import { GroupFeed } from './GroupFeed';
|
import { GroupFeed } from './GroupFeed';
|
||||||
|
import { GroupFlatFeed } from './GroupFlatFeed';
|
||||||
|
|
||||||
|
|
||||||
function GroupHome(props) {
|
function GroupHome(props) {
|
||||||
const {
|
const {
|
||||||
@ -61,13 +63,24 @@ function GroupHome(props) {
|
|||||||
/>
|
/>
|
||||||
) : null }
|
) : null }
|
||||||
<Route path={`${baseUrl}/feed`}>
|
<Route path={`${baseUrl}/feed`}>
|
||||||
<GroupFeed
|
{ (graphMetadata?.vip === 'admin-feed') ? (
|
||||||
graphPath={graphPath}
|
<GroupFeed
|
||||||
groupPath={groupPath}
|
graphPath={graphPath}
|
||||||
vip={graphMetadata?.vip || ''}
|
groupPath={groupPath}
|
||||||
api={api}
|
vip={graphMetadata?.vip || ''}
|
||||||
baseUrl={baseUrl}
|
api={api}
|
||||||
/>
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GroupFlatFeed
|
||||||
|
graphPath={graphPath}
|
||||||
|
groupPath={groupPath}
|
||||||
|
vip={graphMetadata?.vip || ''}
|
||||||
|
api={api}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={baseUrl} exact>
|
<Route path={baseUrl} exact>
|
||||||
<EmptyGroupHome
|
<EmptyGroupHome
|
||||||
|
@ -0,0 +1,235 @@
|
|||||||
|
import { Box, Col } from '@tlon/indigo-react';
|
||||||
|
import { Association, Graph, GraphNode, Group } from '@urbit/api';
|
||||||
|
import bigInt from 'big-integer';
|
||||||
|
import React from 'react';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
|
import ArrayVirtualScroller from '~/views/components/ArrayVirtualScroller';
|
||||||
|
import PostItem from './PostItem/PostItem';
|
||||||
|
import PostInput from './PostInput';
|
||||||
|
|
||||||
|
const virtualScrollerStyle = {
|
||||||
|
height: '100%'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PostFeedProps {
|
||||||
|
graph: Graph;
|
||||||
|
graphPath: string;
|
||||||
|
api: GlobalApi;
|
||||||
|
history: History;
|
||||||
|
baseUrl: string;
|
||||||
|
parentNode?: GraphNode;
|
||||||
|
grandparentNode?: GraphNode;
|
||||||
|
association: Association;
|
||||||
|
group: Group;
|
||||||
|
vip: string;
|
||||||
|
pendingSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
||||||
|
isFetching: boolean;
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.isFetching = false;
|
||||||
|
|
||||||
|
this.fetchPosts = this.fetchPosts.bind(this);
|
||||||
|
this.doNotFetch = this.doNotFetch.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
|
const {
|
||||||
|
graph,
|
||||||
|
graphPath,
|
||||||
|
api,
|
||||||
|
history,
|
||||||
|
baseUrl,
|
||||||
|
parentNode,
|
||||||
|
grandparentNode,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
vip
|
||||||
|
} = this.props;
|
||||||
|
const node = graph.get(index);
|
||||||
|
if (!node) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = graph.peekLargest()?.[0];
|
||||||
|
const post = node?.post;
|
||||||
|
const nodeIndex =
|
||||||
|
( parentNode &&
|
||||||
|
typeof parentNode.post !== 'string'
|
||||||
|
) ? parentNode.post.index.split('/').slice(1).map((ind) => {
|
||||||
|
return bigInt(ind);
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
if (parentNode && index.eq(first ?? bigInt.zero)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index.toString()}>
|
||||||
|
<Col
|
||||||
|
key={index.toString()}
|
||||||
|
ref={ref}
|
||||||
|
mb={3}
|
||||||
|
width="100%"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<PostItem
|
||||||
|
key={parentNode.post.index}
|
||||||
|
parentPost={grandparentNode?.post}
|
||||||
|
node={parentNode}
|
||||||
|
parentNode={grandparentNode}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={nodeIndex}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isParent={true}
|
||||||
|
isRelativeTime={false}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isReply={true}
|
||||||
|
parentPost={parentNode.post}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else if (index.eq(first ?? bigInt.zero)) {
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
width="100%"
|
||||||
|
alignItems="center"
|
||||||
|
key={index.toString()}
|
||||||
|
ref={ref}>
|
||||||
|
<Col
|
||||||
|
width="100%"
|
||||||
|
maxWidth="616px"
|
||||||
|
pt={3}
|
||||||
|
pl={2}
|
||||||
|
pr={2}
|
||||||
|
mb={3}>
|
||||||
|
<PostInput
|
||||||
|
api={api}
|
||||||
|
group={group}
|
||||||
|
association={association}
|
||||||
|
vip={vip}
|
||||||
|
graphPath={graphPath}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
isReply={Boolean(parentNode)}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={index.toString()} ref={ref}>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
isReply={Boolean(parentNode)}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async fetchPosts(newer) {
|
||||||
|
const { graph, graphPath, api } = this.props;
|
||||||
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
|
||||||
|
if (this.isFetching) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetching = true;
|
||||||
|
const { ship, name } = graphResource;
|
||||||
|
const currSize = graph.size;
|
||||||
|
|
||||||
|
if (newer) {
|
||||||
|
const [index] = graph.peekLargest();
|
||||||
|
// TODO: replace this with deep something
|
||||||
|
await api.graph.getYoungerSiblings(
|
||||||
|
ship,
|
||||||
|
name,
|
||||||
|
100,
|
||||||
|
`/${index.toString()}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const [index] = graph.peekSmallest();
|
||||||
|
await api.graph.getOlderSiblings(ship, name, 100, `/${index.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetching = false;
|
||||||
|
return currSize === graph.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doNotFetch(newer) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
flatGraph,
|
||||||
|
pendingSize,
|
||||||
|
parentNode,
|
||||||
|
history
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// TODO: memoize flattening the graph,
|
||||||
|
// take in a prop on whether to flatten the graph or not
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col width="100%" height="100%" position="relative">
|
||||||
|
<ArrayVirtualScroller
|
||||||
|
key={history.location.pathname}
|
||||||
|
origin="top"
|
||||||
|
offset={0}
|
||||||
|
data={flatGraph}
|
||||||
|
averageHeight={106}
|
||||||
|
size={flatGraph.size}
|
||||||
|
style={virtualScrollerStyle}
|
||||||
|
pendingSize={pendingSize}
|
||||||
|
renderer={this.renderItem}
|
||||||
|
loadRows={parentNode ? this.doNotFetch : this.fetchPosts}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(PostFeed);
|
@ -119,8 +119,8 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
width="100%"
|
width="100%"
|
||||||
maxWidth="616px"
|
maxWidth="616px"
|
||||||
pt={3}
|
pt={3}
|
||||||
pl={2}
|
pl={1}
|
||||||
pr={2}
|
pr={1}
|
||||||
mb={3}>
|
mb={3}>
|
||||||
<PostInput
|
<PostInput
|
||||||
api={api}
|
api={api}
|
||||||
@ -182,7 +182,6 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
|
|
||||||
if (newer) {
|
if (newer) {
|
||||||
const [index] = graph.peekLargest();
|
const [index] = graph.peekLargest();
|
||||||
// TODO: replace this with deep something
|
|
||||||
await api.graph.getYoungerSiblings(
|
await api.graph.getYoungerSiblings(
|
||||||
ship,
|
ship,
|
||||||
name,
|
name,
|
||||||
@ -210,9 +209,6 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
history
|
history
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// TODO: memoize flattening the graph,
|
|
||||||
// take in a prop on whether to flatten the graph or not
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col width="100%" height="100%" position="relative">
|
<Col width="100%" height="100%" position="relative">
|
||||||
<VirtualScroller
|
<VirtualScroller
|
||||||
|
@ -0,0 +1,183 @@
|
|||||||
|
import { Box, Col } from '@tlon/indigo-react';
|
||||||
|
import { Association, FlatGraph, FlatGraphNode, Group } from '@urbit/api';
|
||||||
|
import bigInt from 'big-integer';
|
||||||
|
import React from 'react';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
|
import ArrayVirtualScroller, {
|
||||||
|
indexEqual,
|
||||||
|
arrToString
|
||||||
|
} from '~/views/components/ArrayVirtualScroller';
|
||||||
|
import PostItem from './PostItem/PostItem';
|
||||||
|
import PostInput from './PostInput';
|
||||||
|
|
||||||
|
const virtualScrollerStyle = {
|
||||||
|
height: '100%'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PostFeedProps {
|
||||||
|
flatGraph: FlatGraph;
|
||||||
|
graphPath: string;
|
||||||
|
api: GlobalApi;
|
||||||
|
history: History;
|
||||||
|
baseUrl: string;
|
||||||
|
parentNode?: FlatGraphNode;
|
||||||
|
association: Association;
|
||||||
|
group: Group;
|
||||||
|
vip: string;
|
||||||
|
pendingSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostFlatFeed extends React.Component<PostFeedProps, PostFeedState> {
|
||||||
|
isFetching: boolean;
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.isFetching = false;
|
||||||
|
|
||||||
|
this.fetchPosts = this.fetchPosts.bind(this);
|
||||||
|
this.doNotFetch = this.doNotFetch.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
|
const {
|
||||||
|
flatGraph,
|
||||||
|
graphPath,
|
||||||
|
api,
|
||||||
|
history,
|
||||||
|
baseUrl,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
vip
|
||||||
|
} = this.props;
|
||||||
|
const node = flatGraph.get(index);
|
||||||
|
const parentNode = index.length > 1 ?
|
||||||
|
flatGraph.get(index.slice(0, index.length - 1)) : null;
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = flatGraph.peekLargest()?.[0];
|
||||||
|
const post = node?.post;
|
||||||
|
|
||||||
|
if (indexEqual(index, (first ?? [bigInt.zero]))) {
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
width="100%"
|
||||||
|
alignItems="center"
|
||||||
|
key={arrToString(index)}
|
||||||
|
ref={ref}>
|
||||||
|
<Col
|
||||||
|
width="100%"
|
||||||
|
maxWidth="608px"
|
||||||
|
pt={3}
|
||||||
|
pl={1}
|
||||||
|
pr={1}
|
||||||
|
mb={3}>
|
||||||
|
<PostInput
|
||||||
|
api={api}
|
||||||
|
group={group}
|
||||||
|
association={association}
|
||||||
|
vip={vip}
|
||||||
|
graphPath={graphPath}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={index}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
isReply={index.length > 1}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={arrToString(index)} ref={ref}>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={index}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
isReply={index.length > 1}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async fetchPosts(newer) {
|
||||||
|
const { flatGraph, graphPath, api } = this.props;
|
||||||
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
|
||||||
|
if (this.isFetching) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetching = true;
|
||||||
|
const { ship, name } = graphResource;
|
||||||
|
const currSize = flatGraph.size;
|
||||||
|
|
||||||
|
if (newer) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const [index] = flatGraph.peekSmallest();
|
||||||
|
if (index && index.length > 0) {
|
||||||
|
await api.graph.getDeepNewest(ship, name, index[0].toString(), 100);
|
||||||
|
} else {
|
||||||
|
await api.graph.getDeepNewest(ship, name, null, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetching = false;
|
||||||
|
return currSize === flatGraph.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doNotFetch(newer) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
flatGraph,
|
||||||
|
pendingSize,
|
||||||
|
parentNode,
|
||||||
|
history
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col width="100%" height="100%" position="relative">
|
||||||
|
<ArrayVirtualScroller
|
||||||
|
key={history.location.pathname}
|
||||||
|
origin="top"
|
||||||
|
offset={0}
|
||||||
|
data={flatGraph}
|
||||||
|
averageHeight={106}
|
||||||
|
size={flatGraph.size}
|
||||||
|
style={virtualScrollerStyle}
|
||||||
|
pendingSize={pendingSize}
|
||||||
|
renderer={this.renderItem}
|
||||||
|
loadRows={this.fetchPosts}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(PostFlatFeed);
|
@ -0,0 +1,103 @@
|
|||||||
|
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||||
|
import { Association, FlatGraph, Group } from '@urbit/api';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { Loading } from '~/views/components/Loading';
|
||||||
|
import PostFlatFeed from './PostFlatFeed';
|
||||||
|
import PostInput from './PostInput';
|
||||||
|
|
||||||
|
interface PostTimelineProps {
|
||||||
|
api: GlobalApi;
|
||||||
|
association: Association;
|
||||||
|
baseUrl: string;
|
||||||
|
flatGraph: FlatGraph;
|
||||||
|
graphPath: string;
|
||||||
|
group: Group;
|
||||||
|
pendingSize: number;
|
||||||
|
vip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostFlatTimeline = (props: PostTimelineProps): ReactElement => {
|
||||||
|
const {
|
||||||
|
baseUrl,
|
||||||
|
api,
|
||||||
|
association,
|
||||||
|
graphPath,
|
||||||
|
group,
|
||||||
|
flatGraph,
|
||||||
|
pendingSize,
|
||||||
|
vip
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
//console.log(flatGraph);
|
||||||
|
const shouldRenderFeed = Boolean(flatGraph);
|
||||||
|
|
||||||
|
if (!shouldRenderFeed) {
|
||||||
|
return (
|
||||||
|
<Box height="100%" pt={3} pb={3} width="100%" alignItems="center" pl={1}>
|
||||||
|
<Loading />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = flatGraph.peekLargest()?.[0];
|
||||||
|
if (!first) {
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
key={0}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Col
|
||||||
|
width="100%"
|
||||||
|
maxWidth="616px"
|
||||||
|
pt={3}
|
||||||
|
pl={2}
|
||||||
|
pr={2}
|
||||||
|
mb={3}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<PostInput
|
||||||
|
api={api}
|
||||||
|
graphPath={graphPath}
|
||||||
|
group={group}
|
||||||
|
association={association}
|
||||||
|
vip={vip}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Box
|
||||||
|
pl={2}
|
||||||
|
pr={2}
|
||||||
|
width="100%"
|
||||||
|
maxWidth="616px"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Col bg="washedGray" width="100%" alignItems="center" p={3}>
|
||||||
|
<Text textAlign="center" width="100%">
|
||||||
|
No one has posted anything here yet.
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
</Box>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box height="calc(100% - 48px)" width="100%" alignItems="center" pl={1}>
|
||||||
|
<PostFlatFeed
|
||||||
|
key={graphPath}
|
||||||
|
graphPath={graphPath}
|
||||||
|
flatGraph={flatGraph}
|
||||||
|
pendingSize={pendingSize}
|
||||||
|
association={association}
|
||||||
|
group={group}
|
||||||
|
vip={vip}
|
||||||
|
api={api}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostFlatTimeline;
|
@ -0,0 +1,126 @@
|
|||||||
|
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||||
|
import bigInt from 'big-integer';
|
||||||
|
import React from 'react';
|
||||||
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
|
import { Loading } from '~/views/components/Loading';
|
||||||
|
import PostFeed from './PostFeed';
|
||||||
|
import PostItem from './PostItem/PostItem';
|
||||||
|
|
||||||
|
export default function PostThread(props) {
|
||||||
|
const {
|
||||||
|
baseUrl,
|
||||||
|
api,
|
||||||
|
history,
|
||||||
|
association,
|
||||||
|
graphPath,
|
||||||
|
group,
|
||||||
|
vip,
|
||||||
|
pendingSize
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// TODO: make thread
|
||||||
|
return (
|
||||||
|
<Box></Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
|
||||||
|
let graph = props.graph;
|
||||||
|
const shouldRenderFeed = Boolean(graph);
|
||||||
|
|
||||||
|
if (!shouldRenderFeed) {
|
||||||
|
return (
|
||||||
|
<Box height="100%" width="100%" alignItems="center" pl={1} pt={3}>
|
||||||
|
<Loading />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationUrl =
|
||||||
|
props.locationUrl.replace(`${baseUrl}/feed`, '');
|
||||||
|
const nodeIndex = locationUrl.split('/').slice(1).map((ind) => {
|
||||||
|
return bigInt(ind);
|
||||||
|
});
|
||||||
|
|
||||||
|
let node;
|
||||||
|
let parentNode;
|
||||||
|
nodeIndex.forEach((i, idx) => {
|
||||||
|
if (!graph) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
node = graph.get(i);
|
||||||
|
if(idx < nodeIndex.length - 1) {
|
||||||
|
parentNode = node;
|
||||||
|
}
|
||||||
|
if (!node) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
graph = node.children;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!node || !graph) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = graph.peekLargest()?.[0];
|
||||||
|
if (!first) {
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
key={locationUrl}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
alignItems="center" overflowY="scroll"
|
||||||
|
>
|
||||||
|
<Box mt={3} width="100%" alignItems="center">
|
||||||
|
<PostItem
|
||||||
|
key={node.post.index}
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={nodeIndex}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isParent={true}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
pl={2}
|
||||||
|
pr={2}
|
||||||
|
width="100%"
|
||||||
|
maxWidth="616px"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Col bg="washedGray" width="100%" alignItems="center" p={3}>
|
||||||
|
<Text textAlign="center" width="100%">
|
||||||
|
No one has posted any replies yet.
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
</Box>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box height="calc(100% - 48px)" width="100%" alignItems="center" pl={1} pt={3}>
|
||||||
|
<PostFeed
|
||||||
|
key={locationUrl}
|
||||||
|
graphPath={graphPath}
|
||||||
|
graph={graph}
|
||||||
|
grandparentNode={parentNode}
|
||||||
|
parentNode={node}
|
||||||
|
pendingSize={pendingSize}
|
||||||
|
association={association}
|
||||||
|
group={group}
|
||||||
|
vip={vip}
|
||||||
|
api={api}
|
||||||
|
history={history}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,30 +5,21 @@ setAutoFreeze(false);
|
|||||||
|
|
||||||
enablePatches();
|
enablePatches();
|
||||||
|
|
||||||
function sortBigInt(a: BigInteger[], b: BigInteger[]) {
|
export function stringToArr(str: string) {
|
||||||
if (a.lt(b)) {
|
|
||||||
return 1;
|
|
||||||
} else if (a.eq(b)) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringToBigIntArr(str: string) {
|
|
||||||
return str.split('/').slice(1).map((ind) => {
|
return str.split('/').slice(1).map((ind) => {
|
||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrToString(arr: BigInteger[]) {
|
export function arrToString(arr: BigInteger[]) {
|
||||||
let string = '';
|
let string = '';
|
||||||
arr.forEach((key) => {
|
arr.forEach((key) => {
|
||||||
string = string + `/${key.toString()}`;
|
string = string + `/${key.toString()}`;
|
||||||
});
|
});
|
||||||
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortBigIntArr(a: BigInteger[], b: BigInteger[]) {
|
export function sortBigIntArr(a: BigInteger[], b: BigInteger[]) {
|
||||||
let aLen = a.length;
|
let aLen = a.length;
|
||||||
let bLen = b.length;
|
let bLen = b.length;
|
||||||
|
|
||||||
@ -81,7 +72,7 @@ export default class BigIntArrayOrderedMap<V> implements Iterable<[BigInteger[],
|
|||||||
|
|
||||||
set(key: BigInteger[], value: V) {
|
set(key: BigInteger[], value: V) {
|
||||||
return produce(this, draft => {
|
return produce(this, draft => {
|
||||||
draft.root[key.toString()] = castDraft(value);
|
draft.root[arrToString(key)] = castDraft(value);
|
||||||
draft.cachedIter = null;
|
draft.cachedIter = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -138,9 +129,8 @@ export default class BigIntArrayOrderedMap<V> implements Iterable<[BigInteger[],
|
|||||||
return [...this.cachedIter];
|
return [...this.cachedIter];
|
||||||
}
|
}
|
||||||
const result = Object.keys(this.root).map(key => {
|
const result = Object.keys(this.root).map(key => {
|
||||||
const num = stringtoBigIntArr(key);
|
return [stringToArr(key), this.root[key]] as [BigInteger[], V];
|
||||||
return [num, this.root[key]] as [BigInteger[], V];
|
}).sort(([a], [b]) => sortBigIntArr(a, b));
|
||||||
}).sort(([a], [b]) => sortBigIntArr(a,b));
|
|
||||||
this.cachedIter = result;
|
this.cachedIter = result;
|
||||||
return [...result];
|
return [...result];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user