diff --git a/src/platform/web/ui/general/ListView.ts b/src/platform/web/ui/general/ListView.ts index 049ad2a7..16cbce67 100644 --- a/src/platform/web/ui/general/ListView.ts +++ b/src/platform/web/ui/general/ListView.ts @@ -24,7 +24,7 @@ interface IOptions { onItemClick?: (childView: V, evt: UIEvent) => void, className?: string, tagName?: string, - parentProvidesUpdates: boolean + parentProvidesUpdates?: boolean } type SubscriptionHandle = () => undefined; @@ -122,7 +122,7 @@ export class ListView implements UIView { this._childInstances = undefined; } - private loadList() { + protected loadList() { if (!this._list) { return; } @@ -184,4 +184,8 @@ export class ListView implements UIView { protected onBeforeListChanged() {} protected onListChanged() {} + + protected getChildInstanceByIndex(idx: number): V | undefined { + return this._childInstances?.[idx]; + } } diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index 780baef4..e34c1052 100644 --- a/src/platform/web/ui/session/room/MessageComposer.js +++ b/src/platform/web/ui/session/room/MessageComposer.js @@ -17,7 +17,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView.js"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; -import {viewClassForEntry} from "./TimelineView.js" +import {viewClassForEntry} from "./TimelineView" export class MessageComposer extends TemplateView { constructor(viewModel) { diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index faa4637e..3d976ede 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -18,7 +18,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView.js"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; -import {TimelineView} from "./TimelineView.js"; +import {TimelineView} from "./TimelineView"; import {TimelineLoadingView} from "./TimelineLoadingView.js"; import {MessageComposer} from "./MessageComposer.js"; import {RoomArchivedView} from "./RoomArchivedView.js"; diff --git a/src/platform/web/ui/session/room/TimelineView.js b/src/platform/web/ui/session/room/TimelineView.ts similarity index 70% rename from src/platform/web/ui/session/room/TimelineView.js rename to src/platform/web/ui/session/room/TimelineView.ts index 4b2601ce..6768bb8c 100644 --- a/src/platform/web/ui/session/room/TimelineView.js +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -23,8 +23,14 @@ import {FileView} from "./timeline/FileView.js"; import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js"; 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"; -export function viewClassForEntry(entry) { +type TileView = GapView | AnnouncementView | TextMessageView | + ImageView | VideoView | FileView | MissingAttachmentView | RedactedView; +type TileViewConstructor = (this: TileView, SimpleTile) => void; + +export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined { switch (entry.shape) { case "gap": return GapView; case "announcement": return AnnouncementView; @@ -40,13 +46,18 @@ export function viewClassForEntry(entry) { } } -export class TimelineView extends ListView { - constructor(viewModel) { +export class TimelineView extends ListView { + + private _atBottom: boolean; + private _topLoadingPromise?: Promise; + private _viewModel: TimelineViewModel; + + constructor(viewModel: TimelineViewModel) { const options = { className: "Timeline bottom-aligned-scroll", list: viewModel.tiles, onItemClick: (tileView, evt) => tileView.onClick(evt), - } + }; super(options, entry => { const View = viewClassForEntry(entry); if (View) { @@ -54,12 +65,19 @@ export class TimelineView extends ListView { } }); this._atBottom = false; - this._onScroll = this._onScroll.bind(this); - this._topLoadingPromise = null; + this._topLoadingPromise = undefined; this._viewModel = viewModel; } - async _loadAtTopWhile(predicate) { + override handleEvent(evt: Event) { + if (evt.type === "scroll") { + this._handleScroll(evt); + } else { + super.handleEvent(evt); + } + } + + async _loadAtTopWhile(predicate: () => boolean) { if (this._topLoadingPromise) { return; } @@ -78,11 +96,11 @@ export class TimelineView extends ListView { //ignore error, as it is handled in the VM } finally { - this._topLoadingPromise = null; + this._topLoadingPromise = undefined; } } - async _onScroll() { + async _handleScroll(evt: Event) { const PAGINATE_OFFSET = 100; const root = this.root(); if (root.scrollTop < PAGINATE_OFFSET && !this._topLoadingPromise && this._viewModel) { @@ -102,18 +120,18 @@ export class TimelineView extends ListView { } } - mount() { + override mount() { const root = super.mount(); - root.addEventListener("scroll", this._onScroll); + root.addEventListener("scroll", this); return root; } - unmount() { - this.root().removeEventListener("scroll", this._onScroll); + override unmount() { + this.root().removeEventListener("scroll", this); super.unmount(); } - async loadList() { + override async loadList() { super.loadList(); const root = this.root(); // yield so the browser can render the list @@ -130,7 +148,7 @@ export class TimelineView extends ListView { }); } - onBeforeListChanged() { + override onBeforeListChanged() { const fromBottom = this._distanceFromBottom(); this._atBottom = fromBottom < 1; } @@ -140,25 +158,23 @@ export class TimelineView extends ListView { return root.scrollHeight - root.scrollTop - root.clientHeight; } - onListChanged() { + override onListChanged() { const root = this.root(); if (this._atBottom) { root.scrollTop = root.scrollHeight; } } - onUpdate(index, value, param) { + override onUpdate(index: number, value: SimpleTile, param: any) { if (param === "shape") { - if (this._childInstances) { - const ExpectedClass = viewClassForEntry(value); - const child = this._childInstances[index]; - if (!ExpectedClass || !(child instanceof ExpectedClass)) { - // shape was updated, so we need to recreate the tile view, - // the shape parameter is set in EncryptedEventTile.updateEntry - // (and perhaps elsewhere by the time you read this) - super.recreateItem(index, value); - return; - } + const ExpectedClass = viewClassForEntry(value); + const child = this.getChildInstanceByIndex(index); + if (!ExpectedClass || !(child instanceof ExpectedClass)) { + // shape was updated, so we need to recreate the tile view, + // the shape parameter is set in EncryptedEventTile.updateEntry + // (and perhaps elsewhere by the time you read this) + super.recreateItem(index, value); + return; } } super.onUpdate(index, value, param);