From 37597e2acbeb43c04de1e20b6b52256b111801bc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Aug 2020 17:41:10 +0200 Subject: [PATCH 1/6] mark fragment in storage when start of timeline is reached so we don't keep looping to fetch more messages when scrolled all the way up --- .../room/timeline/TimelineViewModel.js | 10 ++++++--- .../session/room/timeline/tiles/GapTile.js | 2 ++ src/matrix/room/Room.js | 3 +++ .../timeline/entries/FragmentBoundaryEntry.js | 21 ++++++++++++++++++- .../room/timeline/persistence/GapWriter.js | 8 +++++++ src/ui/web/session/room/TimelineList.js | 5 ++++- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index d1a4947d..faf93609 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -42,13 +43,16 @@ export class TimelineViewModel { this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId})); } - // doesn't fill gaps, only loads stored entries/tiles - loadAtTop() { + /** + * @return {bool} startReached if the start of the timeline was reached + */ + async loadAtTop() { const firstTile = this._tiles.getFirst(); if (firstTile.shape === "gap") { return firstTile.fill(); } else { - return this._timeline.loadAtTop(50); + await this._timeline.loadAtTop(50); + return false; } } diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index e3ab04b1..b9f5623f 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -41,6 +41,8 @@ export class GapTile extends SimpleTile { this.emitChange("isLoading"); } } + // edgeReached will have been updated by fillGap + return this._entry.edgeReached; } updateEntry(entry, params) { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 1b038972..c922aea4 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -83,6 +83,9 @@ export class Room extends EventEmitter { /** @public */ async fillGap(fragmentEntry, amount) { + if (fragmentEntry.edgeReached) { + return; + } const response = await this._hsApi.messages(this._roomId, { from: fragmentEntry.token, dir: fragmentEntry.direction.asApiString(), diff --git a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js index 9260d970..791c7a9f 100644 --- a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js +++ b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js @@ -60,7 +60,7 @@ export class FragmentBoundaryEntry extends BaseEntry { } get isGap() { - return !!this.token; + return !!this.token && !this.edgeReached; } get token() { @@ -79,6 +79,25 @@ export class FragmentBoundaryEntry extends BaseEntry { } } + get edgeReached() { + if (this.started) { + return this.fragment.startReached; + } else { + return this.fragment.endReached; + } + } + + set edgeReached(reached) { + + if (this.started) { + this.fragment.startReached = reached; + } else { + this.fragment.endReached = reached; + } + } + + + get linkedFragmentId() { if (this.started) { return this.fragment.previousId; diff --git a/src/matrix/room/timeline/persistence/GapWriter.js b/src/matrix/room/timeline/persistence/GapWriter.js index 34b30506..11b774d3 100644 --- a/src/matrix/room/timeline/persistence/GapWriter.js +++ b/src/matrix/room/timeline/persistence/GapWriter.js @@ -178,6 +178,14 @@ export class GapWriter { if (fragmentEntry.token !== start) { throw new Error("start is not equal to prev_batch or next_batch"); } + + // begin (or end) of timeline reached + if (chunk.length === 0) { + fragmentEntry.edgeReached = true; + await txn.timelineFragments.update(fragmentEntry.fragment); + return {entries: [fragmentEntry], fragments: []}; + } + // find last event in fragment so we get the eventIndex to begin creating keys at let lastKey = await this._findFragmentEdgeEventKey(fragmentEntry, txn); // find out if any event in chunk is already present using findFirstOrLastOccurringEventId diff --git a/src/ui/web/session/room/TimelineList.js b/src/ui/web/session/room/TimelineList.js index 49e254cf..a5f75b7c 100644 --- a/src/ui/web/session/room/TimelineList.js +++ b/src/ui/web/session/room/TimelineList.js @@ -45,7 +45,10 @@ export class TimelineList extends ListView { while (predicate()) { // fill, not enough content to fill timeline this._topLoadingPromise = this._viewModel.loadAtTop(); - await this._topLoadingPromise; + const startReached = await this._topLoadingPromise; + if (startReached) { + break; + } } } catch (err) { From 0b1d9bf25e8300e055d88bcba1ff2c4d5e78e123 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Aug 2020 17:41:49 +0200 Subject: [PATCH 2/6] unused now --- src/domain/session/room/timeline/tiles/GapTile.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index b9f5623f..0cfbb491 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -62,14 +62,6 @@ export class GapTile extends SimpleTile { return this._loading; } - get isUp() { - return this._entry.direction.isBackward; - } - - get isDown() { - return this._entry.direction.isForward; - } - get error() { if (this._error) { const dir = this._entry.prev_batch ? "previous" : "next"; From 1c779cf95c7c250f34959918af9e654db47cdf54 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Aug 2020 17:41:57 +0200 Subject: [PATCH 3/6] render encrypted tiles as such so we don't fetch the whole history --- .../room/timeline/tiles/EncryptedEventTile.js | 23 +++++++++++++++++++ .../session/room/timeline/tilesCreator.js | 3 +++ 2 files changed, 26 insertions(+) create mode 100644 src/domain/session/room/timeline/tiles/EncryptedEventTile.js diff --git a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js new file mode 100644 index 00000000..537bd6d9 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js @@ -0,0 +1,23 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {MessageTile} from "./MessageTile.js"; + +export class EncryptedEventTile extends MessageTile { + get text() { + return this.i18n`**Encrypted message**`; + } +} diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js index 56f5faab..2e294467 100644 --- a/src/domain/session/room/timeline/tilesCreator.js +++ b/src/domain/session/room/timeline/tilesCreator.js @@ -20,6 +20,7 @@ import {ImageTile} from "./tiles/ImageTile.js"; import {LocationTile} from "./tiles/LocationTile.js"; import {RoomNameTile} from "./tiles/RoomNameTile.js"; import {RoomMemberTile} from "./tiles/RoomMemberTile.js"; +import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js"; export function tilesCreator({room, ownUserId}) { return function tilesCreator(entry, emitUpdate) { @@ -49,6 +50,8 @@ export function tilesCreator({room, ownUserId}) { return new RoomNameTile(options); case "m.room.member": return new RoomMemberTile(options); + case "m.room.encrypted": + return new EncryptedEventTile(options); default: // unknown type not rendered return null; From df8d884d91257aefb9bdb79f54d9afe7953fe3e5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Aug 2020 17:58:04 +0200 Subject: [PATCH 4/6] append all list items at once for initial render --- src/ui/web/general/ListView.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/web/general/ListView.js b/src/ui/web/general/ListView.js index 38aba94a..cb5a3298 100644 --- a/src/ui/web/general/ListView.js +++ b/src/ui/web/general/ListView.js @@ -102,12 +102,14 @@ export class ListView { } this._subscription = this._list.subscribe(this); this._childInstances = []; + const fragment = document.createDocumentFragment(); for (let item of this._list) { const child = this._childCreator(item); this._childInstances.push(child); const childDomNode = child.mount(this._mountArgs); - this._root.appendChild(childDomNode); + fragment.appendChild(childDomNode); } + this._root.appendChild(fragment); } onAdd(idx, value) { From 4044ac56c59cd9252302b22b2bd111efe49e539a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Aug 2020 17:58:30 +0200 Subject: [PATCH 5/6] actually check the promise --- src/ui/web/session/room/TimelineList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/web/session/room/TimelineList.js b/src/ui/web/session/room/TimelineList.js index a5f75b7c..d350c708 100644 --- a/src/ui/web/session/room/TimelineList.js +++ b/src/ui/web/session/room/TimelineList.js @@ -41,6 +41,9 @@ export class TimelineList extends ListView { } async _loadAtTopWhile(predicate) { + if (this._topLoadingPromise) { + return; + } try { while (predicate()) { // fill, not enough content to fill timeline From 7291cac834862d9882357ae5b183b3ac8e0e8975 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Aug 2020 17:58:39 +0200 Subject: [PATCH 6/6] yield for browser to render before checking content height --- src/ui/web/session/room/TimelineList.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ui/web/session/room/TimelineList.js b/src/ui/web/session/room/TimelineList.js index d350c708..8838963c 100644 --- a/src/ui/web/session/room/TimelineList.js +++ b/src/ui/web/session/room/TimelineList.js @@ -92,9 +92,12 @@ export class TimelineList extends ListView { super.unmount(); } - loadList() { + async loadList() { super.loadList(); const root = this.root(); + // yield so the browser can render the list + // and we can measure the content below + await Promise.resolve(); const {scrollHeight, clientHeight} = root; if (scrollHeight > clientHeight) { root.scrollTop = root.scrollHeight;