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) {
|
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) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue