From a1cf88faba542bbec51a36fb6b99c858dbbcfd30 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Mon, 23 Nov 2020 13:48:36 +1000 Subject: [PATCH] chat-fe: convert virtualscroller to bigInt --- .../src/logic/lib/BigIntOrderedMap.ts | 36 +++++ pkg/interface/src/logic/lib/bigInt.ts | 10 ++ .../apps/chat/components/ChatMessage.tsx | 23 +-- .../views/apps/chat/components/ChatWindow.tsx | 33 +++-- .../apps/chat/components/content/text.js | 4 +- .../src/views/components/RemoteContent.tsx | 5 + .../src/views/components/VirtualScroller.tsx | 136 ++++++++++-------- 7 files changed, 162 insertions(+), 85 deletions(-) create mode 100644 pkg/interface/src/logic/lib/bigInt.ts diff --git a/pkg/interface/src/logic/lib/BigIntOrderedMap.ts b/pkg/interface/src/logic/lib/BigIntOrderedMap.ts index cf7883d97d..069d520020 100644 --- a/pkg/interface/src/logic/lib/BigIntOrderedMap.ts +++ b/pkg/interface/src/logic/lib/BigIntOrderedMap.ts @@ -170,6 +170,42 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { }; return inner(nod); } + + peekLargest(): [BigInteger, V] | undefined { + const inner = (node: MapNode) => { + if(!node) { + return undefined; + } + if(node.l) { + return inner(node.l); + } + return node.n; + } + return inner(this.root); + } + + peekSmallest(): [BigInteger, V] | undefined { + const inner = (node: MapNode) => { + if(!node) { + return undefined; + } + if(node.r) { + return inner(node.r); + } + return node.n; + } + return inner(this.root); + } + + keys(): BigInteger[] { + const list = Array.from(this); + return list.map(([key]) => key); + } + + forEach(f: (value: V, key: BigInteger) => void) { + const list = Array.from(this); + return list.forEach(([k,v]) => f(v,k)); + } [Symbol.iterator](): IterableIterator<[BigInteger, V]> { let result: [BigInteger, V][] = []; diff --git a/pkg/interface/src/logic/lib/bigInt.ts b/pkg/interface/src/logic/lib/bigInt.ts new file mode 100644 index 0000000000..ecc6dda0af --- /dev/null +++ b/pkg/interface/src/logic/lib/bigInt.ts @@ -0,0 +1,10 @@ +import bigInt, { BigInteger } from "big-integer"; + +export function max(a: BigInteger, b: BigInteger) { + return a.gt(b) ? a : b; +} + +export function min(a: BigInteger, b: BigInteger) { + return a.lt(b) ? a : b; +} + diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 4299d39bc6..e6a3ab03a4 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -14,7 +14,7 @@ import RemoteContent from '~/views/components/RemoteContent'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => ( - + New messages below @@ -130,6 +130,7 @@ export default class ChatMessage extends Component { return ( { {renderSigil ? : } - {isLastRead + {isLastRead ? : null} @@ -222,10 +223,11 @@ export class MessageWithSigil extends PureComponent { scrollWindow={scrollWindow} history={history} api={api} - className="fl pr3 v-top pt1" + className="fl pr3 v-top pt1 flex-shrink-0" /> - + { { title={`~${msg.author}`} >{name} {timestamp} - {datestamp} + {datestamp} - + ); @@ -257,8 +260,8 @@ export class MessageWithSigil extends PureComponent { export const MessageWithoutSigil = ({ timestamp, msg, remoteContentPolicy, measure }) => ( <> - {timestamp} - + {timestamp} + @@ -269,7 +272,7 @@ export const MessageContent = ({ content, remoteContentPolicy, measure, fontSize return ; } else if ('url' in content) { return ( - + + {content.me} ); diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index ee56242f7b..a10d682675 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -1,6 +1,7 @@ import React, { Component } from "react"; import { RouteComponentProps } from "react-router-dom"; import _ from "lodash"; +import bigInt, { BigInteger } from 'big-integer'; import GlobalApi from "~/logic/api/global"; import { Patp, Path } from "~/types/noun"; @@ -9,6 +10,7 @@ import { Association } from "~/types/metadata-update"; import { Group } from "~/types/group-update"; import { Envelope, IMessage } from "~/types/chat-update"; import { LocalUpdateRemoteContentPolicy } from "~/types"; +import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap"; import VirtualScroller from "~/views/components/VirtualScroller"; @@ -66,7 +68,7 @@ export default class ChatWindow extends Component prevProps.unreadCount && this.state.idle) { this.setState({ - lastRead: unreadCount ? mailboxSize - unreadCount : Infinity, + lastRead: unreadCount ? mailboxSize - unreadCount : -1, }); } @@ -159,7 +161,7 @@ export default class ChatWindow extends Component a.number - b.number) .forEach(message => { - messages.set(message.number, message); + const num = bigInt(message.number); + messages.set(num, message); lastMessage = message.number; }); stationPendingMessages .sort((a, b) => a.when - b.when) .forEach((message, index) => { - index = index + 1; // To 1-index it - messages.set(mailboxSize + index, message); + const idx = bigInt(index + 1); // To 1-index it + messages.set(bigInt(mailboxSize).add(idx), message); lastMessage = mailboxSize + index; }); @@ -299,24 +302,24 @@ export default class ChatWindow extends Component; + return ; } const isPending: boolean = 'pending' in msg && Boolean(msg.pending); - const isLastMessage: boolean = Boolean(index === lastMessage) - const isLastRead: boolean = Boolean(!isLastMessage && index === this.state.lastRead); - const highlighted = index === this.props.scrollTo; + const isLastMessage: boolean = Boolean(index.eq(bigInt(lastMessage))); + const isLastRead: boolean = Boolean(!isLastMessage && index.eq(bigInt(this.state.lastRead))); + const highlighted = bigInt(this.props.scrollTo || -1).eq(index); const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps }; return ( ); }} loadRows={(start, end) => { - this.fetchMessages(start, end); + this.fetchMessages(start.toJSNumber(), end.toJSNumber()); }} /> diff --git a/pkg/interface/src/views/apps/chat/components/content/text.js b/pkg/interface/src/views/apps/chat/components/content/text.js index 26ec452ace..f616de3a18 100644 --- a/pkg/interface/src/views/apps/chat/components/content/text.js +++ b/pkg/interface/src/views/apps/chat/components/content/text.js @@ -83,7 +83,7 @@ export default class TextContent extends Component { && (urbitOb.isValidPatp(group[2]) // valid patp? && (group[0] === content.text))) { // entire message is room name? return ( - + @@ -93,7 +93,7 @@ export default class TextContent extends Component { ); } else { return ( - + ); diff --git a/pkg/interface/src/views/components/RemoteContent.tsx b/pkg/interface/src/views/components/RemoteContent.tsx index 9c44f381cb..1d8965a908 100644 --- a/pkg/interface/src/views/components/RemoteContent.tsx +++ b/pkg/interface/src/views/components/RemoteContent.tsx @@ -72,6 +72,7 @@ export default class RemoteContent extends PureComponent {this.state.unfold ? 'collapse' : 'expand'} @@ -160,9 +163,11 @@ export default class RemoteContent extends PureComponent void; + scrollWindow: any +} interface VirtualScrollerProps { origin: 'top' | 'bottom'; - loadRows(start: number, end: number): void; - data: Map; - renderer(index): JSX.Element | null; + loadRows(start: BigInteger, end: BigInteger): void; + data: BigIntOrderedMap; + renderer: (props: RendererProps) => JSX.Element | null; onStartReached?(): void; onEndReached?(): void; size: number; - onCalculateVisibleItems?(visibleItems: Map): void; + onCalculateVisibleItems?(visibleItems: BigIntOrderedMap): void; onScroll?({ scrollTop, scrollHeight, windowHeight }): void; style?: any; } interface VirtualScrollerState { startgap: number | undefined; - visibleItems: Map; + visibleItems: BigIntOrderedMap; endgap: number | undefined; totalHeight: number; averageHeight: number; @@ -28,29 +37,29 @@ interface VirtualScrollerState { export default class VirtualScroller extends PureComponent { private scrollContainer: React.RefObject; public window: HTMLDivElement | null; - private cache: Map; + private cache: BigIntOrderedMap; private pendingLoad: { - start: number; - end: number + start: BigInteger; + end: BigInteger timeout: ReturnType; } | undefined; OVERSCAN_SIZE = 100; // Minimum number of messages on either side before loadRows is called - constructor(props) { + constructor(props: VirtualScrollerProps) { super(props); this.state = { startgap: props.origin === 'top' ? 0 : undefined, - visibleItems: new Map(), + visibleItems: new BigIntOrderedMap(), endgap: props.origin === 'bottom' ? 0 : undefined, totalHeight: 0, averageHeight: 64, - scrollTop: props.origin === 'top' ? 0 : Infinity + scrollTop: props.origin === 'top' ? 0 : undefined }; this.scrollContainer = React.createRef(); this.window = null; - this.cache = new Map(); + this.cache = new BigIntOrderedMap(); this.recalculateTotalHeight = this.recalculateTotalHeight.bind(this); this.calculateVisibleItems = this.calculateVisibleItems.bind(this); @@ -75,18 +84,18 @@ export default class VirtualScroller extends PureComponent { + scrollToData(targetIndex: BigInteger): Promise { if (!this.window) { return new Promise((resolve, reject) => {reject()}); } const { offsetHeight } = this.window; let scrollTop = 0; let itemHeight = 0; - new Map([...this.props.data].reverse()).forEach((datum, index) => { + new BigIntOrderedMap([...this.props.data].reverse()).forEach((datum, index) => { const height = this.heightOf(index); - if (index >= targetIndex) { + if (index.geq(targetIndex)) { scrollTop += height; - if (index === targetIndex) { + if (index.eq(targetIndex)) { itemHeight = height; } } @@ -105,20 +114,20 @@ export default class VirtualScroller extends PureComponent(); + let startBuffer = new BigIntOrderedMap(); + let endBuffer = new BigIntOrderedMap(); const { scrollTop, offsetHeight: windowHeight } = this.window; const { averageHeight } = this.state; const { data, size: totalSize, onCalculateVisibleItems } = this.props; - const items = new Map([...data].reverse()); - items.forEach((datum, index) => { + //console.log([...items].map(([index]) => this.heightOf(index))); + const list = [...data]; + console.log(list[0][0].toString()); + // console.log(list[list.length - 1][0].toString()); + [...data].forEach(([index, datum]) => { const height = this.heightOf(index); if (startgap < scrollTop && !startGapFilled) { + console.log(index.toString()); startBuffer.set(index, datum); startgap += height; } else if (heightShown < windowHeight) { startGapFilled = true; visibleItems.set(index, datum); heightShown += height; - } else if (endBuffer.size < visibleItems.size) { + } else if (endBuffer.size < (visibleItems.size - visibleItems.size % 5)) { endBuffer.set(index, data.get(index)); } else { endgap += height; } }); - // endgap += Math.abs(totalSize - data.size) * averageHeight; // Uncomment to make full height of backlog - startBuffer = new Map([...startBuffer].reverse().slice(0, visibleItems.size)); + console.log(startgap); - startBuffer.forEach((datum, index) => { + + startBuffer = new BigIntOrderedMap([...startBuffer].reverse().slice(0, (visibleItems.size - visibleItems.size % 5))); + + + startBuffer.forEach((_datum, index) => { startgap -= this.heightOf(index); }); - visibleItems = new Map([...visibleItems].reverse()); - endBuffer = new Map([...endBuffer].reverse()); - const firstVisibleKey = Array.from(visibleItems.keys())[0] ?? this.estimateIndexFromScrollTop(scrollTop); - const firstNeededKey = Math.max(firstVisibleKey - this.OVERSCAN_SIZE, 0); - if (!data.has(firstNeededKey + 1)) { - this.loadRows(firstNeededKey, firstVisibleKey - 1); + console.log(startBuffer.size); + console.log(startgap); + + const firstVisibleKey = visibleItems.peekLargest()?.[0] ?? this.estimateIndexFromScrollTop(scrollTop)!; + const firstNeededKey = bigIntUtils.max(firstVisibleKey.subtract(bigInt(this.OVERSCAN_SIZE)), bigInt.zero) + if (!data.has(firstNeededKey.add(bigInt.one))) { + this.loadRows(firstNeededKey, firstVisibleKey.subtract(bigInt.one)); } - const lastVisibleKey = Array.from(visibleItems.keys())[visibleItems.size - 1] ?? this.estimateIndexFromScrollTop(scrollTop + windowHeight); - const lastNeededKey = Math.min(lastVisibleKey + this.OVERSCAN_SIZE, totalSize); - if (!data.has(lastNeededKey - 1)) { - this.loadRows(lastVisibleKey + 1, lastNeededKey); + const lastVisibleKey = + visibleItems.peekSmallest()?.[0] + ?? bigInt(this.estimateIndexFromScrollTop(scrollTop + windowHeight)!); + const lastNeededKey = bigIntUtils.min(lastVisibleKey.add(bigInt(this.OVERSCAN_SIZE)), bigInt(totalSize)); + + if (!data.has(lastNeededKey.subtract(bigInt.one))) { + this.loadRows(lastVisibleKey.add(bigInt.one), lastNeededKey); } onCalculateVisibleItems ? onCalculateVisibleItems(visibleItems) : null; this.setState({ startgap: Number(startgap.toFixed()), - visibleItems: new Map([...endBuffer, ...visibleItems, ...startBuffer]), + visibleItems: new BigIntOrderedMap([...startBuffer, ...visibleItems, ...endBuffer]), endgap: Number(endgap.toFixed()), }); } - loadRows(start, end) { - if (isNaN(start) || isNaN(end)) return; + loadRows(start: BigInteger, end: BigInteger) { if (this.pendingLoad?.timeout) { clearTimeout(this.pendingLoad.timeout); - start = Math.min(start, this.pendingLoad.start); - end = Math.max(end, this.pendingLoad.end); + start = bigIntUtils.min(start, this.pendingLoad.start); + end = bigIntUtils.max(end, this.pendingLoad.end); } this.pendingLoad = { timeout: setTimeout(() => { if (!this.pendingLoad) return; - start = Math.max(this.pendingLoad.start, 0); - end = Math.min(Math.max(this.pendingLoad.end, 0), this.props.size); + start = bigIntUtils.max(this.pendingLoad.start, bigInt.zero); + end = bigIntUtils.min(bigIntUtils.max(this.pendingLoad.end, bigInt.zero), bigInt(this.props.size)); if (start < end) { this.props.loadRows(start, end); } @@ -204,11 +223,11 @@ export default class VirtualScroller extends PureComponent { + /* 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.resetScroll(); @@ -304,12 +324,12 @@ export default class VirtualScroller extends PureComponent { - const measure = (element) => { + const render = (index: BigInteger) => { + const measure = (element: any) => { if (element) { this.cache.set(index, { height: element.offsetHeight,