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) {
const changed = this._replyVM?.internalId !== entry?.asEventKey().toString();
const changed = this._replyVM?.id?.equals(entry?.asEventKey());
if (changed) {
this._replyVM = this.disposeTracked(this._replyVM);
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 {ViewModel} from "../../../ViewModel.js";
export class TimelineViewModel extends ViewModel {
constructor(options) {
super(options);
@ -43,11 +45,8 @@ export class TimelineViewModel extends ViewModel {
this._timeline.loadAtTop(50);
}
setVisibleTileRange(idx, len) {
console.log("setVisibleTileRange", idx, len);
if (idx < 5) {
this._timeline.loadAtTop(10);
}
setVisibleTileRange(startId, endId) {
console.log("setVisibleTileRange", startId, endId);
}
get tiles() {

View File

@ -40,8 +40,8 @@ export class SimpleTile extends ViewModel {
return false;
}
get internalId() {
return this._entry.asEventKey().toString();
get id() {
return this._entry.asEventKey();
}
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];
}
}

View File

@ -26,7 +26,7 @@ import {AnnouncementView} from "./timeline/AnnouncementView.js";
import {RedactedView} from "./timeline/RedactedView.js";
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.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 |
ImageView | VideoView | FileView | MissingAttachmentView | RedactedView;
@ -59,7 +59,8 @@ function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex
return i;
}
}
return -1;
// return first item if nothing matched before
return 0;
}
export class TimelineView extends TemplateView<TimelineViewModel> {
@ -67,20 +68,25 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
private anchoredNode?: HTMLElement;
private anchoredBottom: number = 0;
private stickToBottom: boolean = true;
private tilesView?: TilesListView;
render(t: TemplateBuilder, vm: TimelineViewModel) {
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
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 tiles = timeline.firstElementChild as HTMLElement;
const tiles = this.tilesView!.root() as HTMLElement;
const missingTilesHeight = timeline.clientHeight - tiles.clientHeight;
if (missingTilesHeight > 0) {
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 {
tiles.style.removeProperty("margin-top");
if (this.stickToBottom) {
@ -102,21 +108,30 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
private onScroll(): void {
const timeline = this.root() as HTMLElement;
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;
if (!this.stickToBottom) {
// save bottom node position
if (this.stickToBottom) {
const len = this.value.tiles.length;
bottomNodeIndex = len - 1;
} else {
const viewportBottom = scrollTop + clientHeight;
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.classList.add("pinned");
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 */
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) {
if (this._menuPopup && this._menuPopup.isOpen) {
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))
]);
}
/* 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; }
}