diff --git a/frontend/src/Connection.ts b/frontend/src/Connection.ts index 814d863..de15c07 100644 --- a/frontend/src/Connection.ts +++ b/frontend/src/Connection.ts @@ -149,7 +149,7 @@ export class Connection { public handleMessages = (messages: FeedMessage.Message[]) => { const { nodes, chains, sortBy, selectedColumns } = this.state; - const ref = nodes.ref(); + const { ref } = nodes; const updateState: UpdateBound = (state) => { this.state = this.update(state); diff --git a/frontend/src/common/SortedCollection.ts b/frontend/src/common/SortedCollection.ts index d1d9ddb..e9ba377 100644 --- a/frontend/src/common/SortedCollection.ts +++ b/frontend/src/common/SortedCollection.ts @@ -97,11 +97,27 @@ export namespace SortedCollection { export type StateRef = Opaque; } +interface Focus { + start: number; + end: number; +} + export class SortedCollection { + // Comparator function used to sort the collection private compare: Compare; + // Mapping item `id` to the `Item`, this uses array as a structure with + // the assumption that `id`s provided are increments from `0`, and that + // vacant `id`s will be re-used in the future. private map = Array>(); + // Actual sorted list of `Item`s. private list = Array(); + // Internal tracker for changes, this number increments whenever the + // order of the **focused** elements in the collection changes private changeRef = 0; + // Marks the range of indicies that are focused for tracking. + // **Note:** `start` is inclusive, while `end` is exclusive (much like + // `Array.slice()`). + private focus: Focus = { start: 0, end: 0 }; constructor(compare: Compare) { this.compare = compare; @@ -114,10 +130,6 @@ export class SortedCollection { this.changeRef += 1; } - public ref(): SortedCollection.StateRef { - return this.changeRef as SortedCollection.StateRef; - } - public add(item: Item) { if (this.map.length <= item.id) { // Grow map if item.id would be out of scope @@ -131,9 +143,11 @@ export class SortedCollection { this.map[item.id] = item; - sortedInsert(item, this.list, this.compare); + const index = sortedInsert(item, this.list, this.compare); - this.changeRef += 1; + if (index < this.focus.end) { + this.changeRef += 1; + } } public remove(id: number) { @@ -147,7 +161,9 @@ export class SortedCollection { this.list.splice(index, 1); this.map[id] = null; - this.changeRef += 1; + if (index < this.focus.end) { + this.changeRef += 1; + } } public get(id: number): Maybe { @@ -184,7 +200,13 @@ export class SortedCollection { const newIndex = sortedInsert(item, this.list, this.compare); if (newIndex !== index) { - this.changeRef += 1; + const outOfFocus = + (index < this.focus.start && newIndex < this.focus.start) || + (index >= this.focus.end && newIndex >= this.focus.end); + + if (!outOfFocus) { + this.changeRef += 1; + } } } @@ -207,6 +229,7 @@ export class SortedCollection { public mutEachAndSort(mutator: (item: Item) => void) { this.list.forEach(mutator); this.list.sort(this.compare); + this.changeRef += 1; } public clear() { @@ -216,6 +239,18 @@ export class SortedCollection { this.changeRef += 1; } + // Set a new `Focus`. Any changes to the order of items within the `Focus` + // will increment `changeRef`. + public setFocus(start: number, end: number) { + this.focus = { start, end }; + } + + // Get the reference to current ordering state of focused items. + public get ref(): SortedCollection.StateRef { + return this.changeRef as SortedCollection.StateRef; + } + + // Check if order of focused items has changed since obtaining a `ref`. public hasChangedSince(ref: SortedCollection.StateRef): boolean { return this.changeRef > ref; } diff --git a/frontend/src/components/List/List.tsx b/frontend/src/components/List/List.tsx index 71c8795..12c7b36 100644 --- a/frontend/src/components/List/List.tsx +++ b/frontend/src/components/List/List.tsx @@ -52,11 +52,11 @@ export class List extends React.Component { } public render() { - const { selectedColumns } = this.props.appState; - const { pins, sortBy } = this.props; - const { filter } = this.state; + const { pins, sortBy, appState } = this.props; + const { selectedColumns } = appState; + const { filter, listStart, listEnd } = this.state; - let nodes = this.props.appState.nodes.sorted(); + let nodes = appState.nodes.sorted(); if (filter != null) { nodes = nodes.filter(filter); @@ -73,10 +73,13 @@ export class List extends React.Component { ); } + // With filter present, we can no longer guarantee that focus corresponds + // to rendering view, so we put the whole list in focus + appState.nodes.setFocus(0, nodes.length); + } else { + appState.nodes.setFocus(listStart, listEnd); } - const { listStart, listEnd } = this.state; - const height = TH_HEIGHT + nodes.length * TR_HEIGHT; const transform = `translateY(${listStart * TR_HEIGHT}px)`;