forked from mystiq/hydrogen-web
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:
parent
c78a83d398
commit
b3cd2a0e03
8 changed files with 48 additions and 22 deletions
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -85,6 +85,9 @@ export class BaseMessageView extends TemplateView {
|
|||
this._toggleMenu(evt.target);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue