express the visible range with EventKeys rather than list indices

This is less ambiguous in case the DOM and the ObservableList would be
out of sync.
This commit is contained in:
Bruno Windels 2021-09-07 17:48:49 +02:00
parent c78a83d398
commit b3cd2a0e03
8 changed files with 48 additions and 22 deletions

View file

@ -25,7 +25,7 @@ export class ComposerViewModel extends ViewModel {
} }
setReplyingTo(entry) { setReplyingTo(entry) {
const changed = this._replyVM?.internalId !== entry?.asEventKey().toString(); const changed = this._replyVM?.id?.equals(entry?.asEventKey());
if (changed) { if (changed) {
this._replyVM = this.disposeTracked(this._replyVM); this._replyVM = this.disposeTracked(this._replyVM);
if (entry) { if (entry) {

View file

@ -34,6 +34,8 @@ when loading, it just reads events from a sortkey backwards or forwards...
import {TilesCollection} from "./TilesCollection.js"; import {TilesCollection} from "./TilesCollection.js";
import {ViewModel} from "../../../ViewModel.js"; import {ViewModel} from "../../../ViewModel.js";
export class TimelineViewModel extends ViewModel { export class TimelineViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
@ -43,11 +45,8 @@ export class TimelineViewModel extends ViewModel {
this._timeline.loadAtTop(50); this._timeline.loadAtTop(50);
} }
setVisibleTileRange(idx, len) { setVisibleTileRange(startId, endId) {
console.log("setVisibleTileRange", idx, len); console.log("setVisibleTileRange", startId, endId);
if (idx < 5) {
this._timeline.loadAtTop(10);
}
} }
get tiles() { get tiles() {

View file

@ -40,8 +40,8 @@ export class SimpleTile extends ViewModel {
return false; return false;
} }
get internalId() { get id() {
return this._entry.asEventKey().toString(); return this._entry.asEventKey();
} }
get isPending() { get isPending() {

View file

@ -176,7 +176,7 @@ export class ListView<T, V extends UIView> implements UIView {
} }
} }
protected getChildInstanceByIndex(idx: number): V | undefined { public getChildInstanceByIndex(idx: number): V | undefined {
return this._childInstances?.[idx]; return this._childInstances?.[idx];
} }
} }

View file

@ -26,7 +26,7 @@ import {AnnouncementView} from "./timeline/AnnouncementView.js";
import {RedactedView} from "./timeline/RedactedView.js"; import {RedactedView} from "./timeline/RedactedView.js";
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js"; import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js"; import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
import {BaseObservableList as ObservableList} from "../../../../../../observable/list/BaseObservableList.js"; import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList.js";
type TileView = GapView | AnnouncementView | TextMessageView | type TileView = GapView | AnnouncementView | TextMessageView |
ImageView | VideoView | FileView | MissingAttachmentView | RedactedView; ImageView | VideoView | FileView | MissingAttachmentView | RedactedView;
@ -59,7 +59,8 @@ function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex
return i; return i;
} }
} }
return -1; // return first item if nothing matched before
return 0;
} }
export class TimelineView extends TemplateView<TimelineViewModel> { export class TimelineView extends TemplateView<TimelineViewModel> {
@ -67,20 +68,25 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
private anchoredNode?: HTMLElement; private anchoredNode?: HTMLElement;
private anchoredBottom: number = 0; private anchoredBottom: number = 0;
private stickToBottom: boolean = true; private stickToBottom: boolean = true;
private tilesView?: TilesListView;
render(t: TemplateBuilder, vm: TimelineViewModel) { render(t: TemplateBuilder, vm: TimelineViewModel) {
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
return t.div({className: "Timeline bottom-aligned-scroll", onScroll: () => this.onScroll()}, [ return t.div({className: "Timeline bottom-aligned-scroll", onScroll: () => this.onScroll()}, [
t.view(new TilesListView(vm.tiles, () => this._restoreScrollPosition())) t.view(this.tilesView)
]); ]);
} }
private _restoreScrollPosition() { private restoreScrollPosition() {
const timeline = this.root() as HTMLElement; const timeline = this.root() as HTMLElement;
const tiles = timeline.firstElementChild as HTMLElement; const tiles = this.tilesView!.root() as HTMLElement;
const missingTilesHeight = timeline.clientHeight - tiles.clientHeight; const missingTilesHeight = timeline.clientHeight - tiles.clientHeight;
if (missingTilesHeight > 0) { if (missingTilesHeight > 0) {
tiles.style.setProperty("margin-top", `${missingTilesHeight}px`); tiles.style.setProperty("margin-top", `${missingTilesHeight}px`);
// we don't have enough tiles to fill the viewport, so set all as visible
const len = this.value.tiles.length;
this.updateVisibleRange(0, len - 1);
} else { } else {
tiles.style.removeProperty("margin-top"); tiles.style.removeProperty("margin-top");
if (this.stickToBottom) { if (this.stickToBottom) {
@ -102,21 +108,30 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
private onScroll(): void { private onScroll(): void {
const timeline = this.root() as HTMLElement; const timeline = this.root() as HTMLElement;
const {scrollHeight, scrollTop, clientHeight} = timeline; const {scrollHeight, scrollTop, clientHeight} = timeline;
const tiles = timeline.firstElementChild as HTMLElement; const tiles = this.tilesView!.root() as HTMLElement;
let bottomNodeIndex;
this.stickToBottom = Math.abs(scrollHeight - (scrollTop + clientHeight)) < 5; this.stickToBottom = Math.abs(scrollHeight - (scrollTop + clientHeight)) < 5;
if (!this.stickToBottom) { if (this.stickToBottom) {
// save bottom node position const len = this.value.tiles.length;
bottomNodeIndex = len - 1;
} else {
const viewportBottom = scrollTop + clientHeight; const viewportBottom = scrollTop + clientHeight;
const anchoredNodeIndex = findFirstNodeIndexAtOrBelow(tiles, viewportBottom); const anchoredNodeIndex = findFirstNodeIndexAtOrBelow(tiles, viewportBottom);
let topNodeIndex = findFirstNodeIndexAtOrBelow(tiles, scrollTop, anchoredNodeIndex);
if (topNodeIndex === -1) {
topNodeIndex = 0;
}
this.anchoredNode = tiles.childNodes[anchoredNodeIndex] as HTMLElement; this.anchoredNode = tiles.childNodes[anchoredNodeIndex] as HTMLElement;
this.anchoredNode.classList.add("pinned"); this.anchoredNode.classList.add("pinned");
this.anchoredBottom = bottom(this.anchoredNode!); this.anchoredBottom = bottom(this.anchoredNode!);
this.value.setVisibleTileRange(topNodeIndex, anchoredNodeIndex - topNodeIndex); bottomNodeIndex = anchoredNodeIndex;
}
let topNodeIndex = findFirstNodeIndexAtOrBelow(tiles, scrollTop, bottomNodeIndex);
this.updateVisibleRange(topNodeIndex, bottomNodeIndex);
}
private updateVisibleRange(startIndex: number, endIndex: number) {
const firstVisibleChild = this.tilesView!.getChildInstanceByIndex(startIndex);
const lastVisibleChild = this.tilesView!.getChildInstanceByIndex(endIndex);
if (firstVisibleChild && lastVisibleChild) {
this.value.setVisibleTileRange(firstVisibleChild.id, lastVisibleChild.id);
} }
} }
} }

View file

@ -23,4 +23,7 @@ export class AnnouncementView extends TemplateView {
/* This is called by the parent ListView, which just has 1 listener for the whole list */ /* This is called by the parent ListView, which just has 1 listener for the whole list */
onClick() {} onClick() {}
/** Used by TimelineView to get the id of a tile when setting the visible range */
get id() { return this.value.id; }
} }

View file

@ -86,6 +86,9 @@ export class BaseMessageView extends TemplateView {
} }
} }
/** Used by TimelineView to get the id of a tile when setting the visible range */
get id() { return this.value.id; }
_toggleMenu(button) { _toggleMenu(button) {
if (this._menuPopup && this._menuPopup.isOpen) { if (this._menuPopup && this._menuPopup.isOpen) {
this._menuPopup.close(); this._menuPopup.close();

View file

@ -29,4 +29,10 @@ export class GapView extends TemplateView {
t.if(vm => vm.error, t => t.strong(vm => vm.error)) t.if(vm => vm.error, t => t.strong(vm => vm.error))
]); ]);
} }
/* This is called by the parent ListView, which just has 1 listener for the whole list */
onClick() {}
/** Used by TimelineView to get the id of a tile when setting the visible range */
get id() { return this.value.id; }
} }