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) {
|
||||
const start = startTime ? decToUd(startTime) : null;
|
||||
const start = startTime ? decToUd(startTime) : 'null';
|
||||
const data = await this.scry<any>('graph-store',
|
||||
`/deep-nodes-up-to/${ship}/${resource}/${count}/${start}`
|
||||
);
|
||||
|
@ -26,9 +26,8 @@ export const GraphReducer = (json) => {
|
||||
|
||||
const flat = _.get(json, 'graph-update-flat', false);
|
||||
if (flat) {
|
||||
reduceState<GraphState, any>(useGraphState, loose, [addNodesFlat]);
|
||||
reduceState<GraphState, any>(useGraphState, flat, [addNodesFlat]);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
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));
|
||||
|
||||
indices.forEach((index) => {
|
||||
const node = data.nodes[index];
|
||||
let node = data.nodes[index];
|
||||
if (index.split('/').length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -73,14 +72,11 @@ const addNodesFlat = (json: any, state: GraphState): GraphState => {
|
||||
return state;
|
||||
}
|
||||
|
||||
node.children = mapifyChildren({});
|
||||
state.flatGraphs[resource] =
|
||||
state.flatGraphs[resource].set(
|
||||
indexArr,
|
||||
produce(node, (draft) => {
|
||||
draft.children = mapifyChildren({});
|
||||
})
|
||||
);
|
||||
state.flatGraphs[resource].set(indexArr, node);
|
||||
});
|
||||
|
||||
}
|
||||
return state;
|
||||
};
|
||||
@ -120,8 +116,13 @@ const addGraph = (json, state: GraphState): GraphState => {
|
||||
state.graphs = {};
|
||||
}
|
||||
|
||||
if (!('flatGraphs' in state)) {
|
||||
state.flatGraphs = {};
|
||||
}
|
||||
|
||||
const resource = data.resource.ship + '/' + data.resource.name;
|
||||
state.graphs[resource] = new BigIntOrderedMap();
|
||||
state.flatGraphs[resource] = new BigIntArrayOrderedMap();
|
||||
state.graphTimesentMap[resource] = {};
|
||||
|
||||
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)) {
|
||||
state.graphs = {};
|
||||
}
|
||||
|
||||
if (!('graphs' in state)) {
|
||||
state.flatGraphs = {};
|
||||
}
|
||||
const resource = data.ship + '/' + data.name;
|
||||
state.graphKeys.delete(resource);
|
||||
delete state.graphs[resource];
|
||||
@ -219,14 +224,18 @@ const addNodes = (json, state) => {
|
||||
const data = _.get(json, 'add-nodes', false);
|
||||
if (data) {
|
||||
if (!('graphs' in state)) {
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const resource = data.resource.ship + '/' + data.resource.name;
|
||||
if (!(resource in state.graphs)) {
|
||||
state.graphs[resource] = new BigIntOrderedMap();
|
||||
}
|
||||
|
||||
if (!(resource in state.flatGraphs)) {
|
||||
state.flatGraphs[resource] = new BigIntArrayOrderedMap();
|
||||
}
|
||||
|
||||
if (!(resource in state.graphTimesentMap)) {
|
||||
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 { BaseState, createState } from './base';
|
||||
|
||||
@ -27,6 +27,7 @@ export interface GraphState extends BaseState<GraphState> {
|
||||
|
||||
const useGraphState = createState<GraphState>('Graph', {
|
||||
graphs: {},
|
||||
flatGraphs: {},
|
||||
graphKeys: new Set(),
|
||||
looseNodes: {},
|
||||
pendingIndices: {},
|
||||
@ -130,7 +131,7 @@ const useGraphState = createState<GraphState>('Graph', {
|
||||
// });
|
||||
// graphReducer(node);
|
||||
// },
|
||||
}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap']);
|
||||
}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap', 'flatGraphs']);
|
||||
|
||||
export function useGraph(ship: string, name: string) {
|
||||
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}
|
||||
tall={tall}
|
||||
>
|
||||
{children.map((c) => (
|
||||
{children.map((c, idx) => (
|
||||
<Graphdown
|
||||
key={idx}
|
||||
transcluded={transcluded}
|
||||
depth={depth + 1}
|
||||
{...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 { EnableGroupFeed } from './EnableGroupFeed';
|
||||
import { GroupFeed } from './GroupFeed';
|
||||
import { GroupFlatFeed } from './GroupFlatFeed';
|
||||
|
||||
|
||||
function GroupHome(props) {
|
||||
const {
|
||||
@ -61,13 +63,24 @@ function GroupHome(props) {
|
||||
/>
|
||||
) : null }
|
||||
<Route path={`${baseUrl}/feed`}>
|
||||
<GroupFeed
|
||||
graphPath={graphPath}
|
||||
groupPath={groupPath}
|
||||
vip={graphMetadata?.vip || ''}
|
||||
api={api}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
{ (graphMetadata?.vip === 'admin-feed') ? (
|
||||
<GroupFeed
|
||||
graphPath={graphPath}
|
||||
groupPath={groupPath}
|
||||
vip={graphMetadata?.vip || ''}
|
||||
api={api}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
) : (
|
||||
<GroupFlatFeed
|
||||
graphPath={graphPath}
|
||||
groupPath={groupPath}
|
||||
vip={graphMetadata?.vip || ''}
|
||||
api={api}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Route>
|
||||
<Route path={baseUrl} exact>
|
||||
<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%"
|
||||
maxWidth="616px"
|
||||
pt={3}
|
||||
pl={2}
|
||||
pr={2}
|
||||
pl={1}
|
||||
pr={1}
|
||||
mb={3}>
|
||||
<PostInput
|
||||
api={api}
|
||||
@ -182,7 +182,6 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
||||
|
||||
if (newer) {
|
||||
const [index] = graph.peekLargest();
|
||||
// TODO: replace this with deep something
|
||||
await api.graph.getYoungerSiblings(
|
||||
ship,
|
||||
name,
|
||||
@ -210,9 +209,6 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
||||
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">
|
||||
<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();
|
||||
|
||||
function sortBigInt(a: BigInteger[], b: BigInteger[]) {
|
||||
if (a.lt(b)) {
|
||||
return 1;
|
||||
} else if (a.eq(b)) {
|
||||
return 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function stringToBigIntArr(str: string) {
|
||||
export function stringToArr(str: string) {
|
||||
return str.split('/').slice(1).map((ind) => {
|
||||
return bigInt(ind);
|
||||
});
|
||||
}
|
||||
|
||||
function arrToString(arr: BigInteger[]) {
|
||||
export function arrToString(arr: BigInteger[]) {
|
||||
let string = '';
|
||||
arr.forEach((key) => {
|
||||
string = string + `/${key.toString()}`;
|
||||
});
|
||||
return string;
|
||||
}
|
||||
|
||||
function sortBigIntArr(a: BigInteger[], b: BigInteger[]) {
|
||||
export function sortBigIntArr(a: BigInteger[], b: BigInteger[]) {
|
||||
let aLen = a.length;
|
||||
let bLen = b.length;
|
||||
|
||||
@ -81,7 +72,7 @@ export default class BigIntArrayOrderedMap<V> implements Iterable<[BigInteger[],
|
||||
|
||||
set(key: BigInteger[], value: V) {
|
||||
return produce(this, draft => {
|
||||
draft.root[key.toString()] = castDraft(value);
|
||||
draft.root[arrToString(key)] = castDraft(value);
|
||||
draft.cachedIter = null;
|
||||
});
|
||||
}
|
||||
@ -138,9 +129,8 @@ export default class BigIntArrayOrderedMap<V> implements Iterable<[BigInteger[],
|
||||
return [...this.cachedIter];
|
||||
}
|
||||
const result = Object.keys(this.root).map(key => {
|
||||
const num = stringtoBigIntArr(key);
|
||||
return [num, this.root[key]] as [BigInteger[], V];
|
||||
}).sort(([a], [b]) => sortBigIntArr(a,b));
|
||||
return [stringToArr(key), this.root[key]] as [BigInteger[], V];
|
||||
}).sort(([a], [b]) => sortBigIntArr(a, b));
|
||||
this.cachedIter = result;
|
||||
return [...result];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user