diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js
index baf9b6e3..5938ada5 100644
--- a/src/domain/session/room/timeline/TimelineViewModel.js
+++ b/src/domain/session/room/timeline/TimelineViewModel.js
@@ -46,6 +46,7 @@ export class TimelineViewModel extends ViewModel {
this._requestedStartTile = null;
this._requestedEndTile = null;
this._requestScheduled = false;
+ this._showJumpDown = false;
}
/** if this.tiles is empty, call this with undefined for both startTile and endTile */
@@ -75,10 +76,12 @@ export class TimelineViewModel extends ViewModel {
tile.notifyVisible();
}
loadTop = startIndex < 10;
+ this._setShowJumpDown(endIndex < (this._tiles.length - 1));
// console.log("got tiles", startIndex, endIndex, loadTop);
} else {
// tiles collection is empty, load more at top
loadTop = true;
+ this._setShowJumpDown(false);
// console.log("no tiles, load more at top");
}
@@ -100,4 +103,15 @@ export class TimelineViewModel extends ViewModel {
get tiles() {
return this._tiles;
}
+
+ _setShowJumpDown(show) {
+ if (this._showJumpDown !== show) {
+ this._showJumpDown = show;
+ this.emitChange("showJumpDown");
+ }
+ }
+
+ get showJumpDown() {
+ return this._showJumpDown;
+ }
}
diff --git a/src/platform/web/ui/css/themes/element/icons/chevron-down.svg b/src/platform/web/ui/css/themes/element/icons/chevron-down.svg
new file mode 100644
index 00000000..d2068199
--- /dev/null
+++ b/src/platform/web/ui/css/themes/element/icons/chevron-down.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css
index 1caf09d5..d68d7ff5 100644
--- a/src/platform/web/ui/css/themes/element/timeline.css
+++ b/src/platform/web/ui/css/themes/element/timeline.css
@@ -15,6 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+.Timeline_jumpDown {
+ width: 40px;
+ height: 40px;
+ bottom: 16px;
+ right: 32px;
+ border-radius: 100%;
+ border: 1px solid #8d99a5;
+ background-image: url(icons/chevron-down.svg);
+ background-position: center;
+ background-color: white;
+ background-repeat: no-repeat;
+ cursor: pointer;
+}
+
.Timeline_message {
display: grid;
grid-template:
diff --git a/src/platform/web/ui/css/timeline.css b/src/platform/web/ui/css/timeline.css
index dd34ba05..c4d1459b 100644
--- a/src/platform/web/ui/css/timeline.css
+++ b/src/platform/web/ui/css/timeline.css
@@ -14,8 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+.Timeline {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
-.RoomView_body > .Timeline {
+.Timeline_jumpDown {
+ position: absolute;
+}
+
+.Timeline_scroller {
overflow-y: scroll;
overscroll-behavior-y: contain;
overflow-anchor: none;
@@ -23,9 +32,11 @@ limitations under the License.
margin: 0;
/* need to read the offsetTop of tiles relative to this element in TimelineView */
position: relative;
+ min-height: 0;
+ flex: 1 0 0;
}
-.RoomView_body > .Timeline > ul {
+.Timeline_scroller > ul {
list-style: none;
/* use small horizontal padding so first/last children margin isn't collapsed
at the edge and a scrollbar shows up when setting margin-top to bottom-align
diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts
index 61fbca08..7446dbcb 100644
--- a/src/platform/web/ui/session/room/TimelineView.ts
+++ b/src/platform/web/ui/session/room/TimelineView.ts
@@ -78,8 +78,19 @@ export class TimelineView extends TemplateView {
this.restoreScrollPosition();
});
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
- const root = t.div({className: "Timeline bottom-aligned-scroll", onScroll: () => this.onScroll()}, [
- t.view(this.tilesView)
+ const root = t.div({className: "Timeline"}, [
+ t.div({
+ className: "Timeline_scroller bottom-aligned-scroll",
+ onScroll: () => this.onScroll()
+ }, t.view(this.tilesView)),
+ t.button({
+ className: {
+ "Timeline_jumpDown": true,
+ hidden: vm => !vm.showJumpDown
+ },
+ title: "Jump down",
+ onClick: () => this.jumpDown()
+ })
]);
if (typeof ResizeObserver === "function") {
@@ -92,6 +103,16 @@ export class TimelineView extends TemplateView {
return root;
}
+ private get scroller() {
+ return this.root().firstElementChild as HTMLElement;
+ }
+
+ private jumpDown() {
+ const {scroller} = this;
+ this.stickToBottom = true;
+ scroller.scrollTop = scroller.scrollHeight;
+ }
+
public unmount() {
super.unmount();
if (this.resizeObserver) {
@@ -101,10 +122,10 @@ export class TimelineView extends TemplateView {
}
private restoreScrollPosition() {
- const timeline = this.root() as HTMLElement;
+ const {scroller} = this;
const tiles = this.tilesView!.root() as HTMLElement;
- const missingTilesHeight = timeline.clientHeight - tiles.clientHeight;
+ const missingTilesHeight = scroller.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
@@ -113,23 +134,20 @@ export class TimelineView extends TemplateView {
} else {
tiles.style.removeProperty("margin-top");
if (this.stickToBottom) {
- timeline.scrollTop = timeline.scrollHeight;
+ scroller.scrollTop = scroller.scrollHeight;
} else if (this.anchoredNode) {
const newAnchoredBottom = bottom(this.anchoredNode!);
if (newAnchoredBottom !== this.anchoredBottom) {
const bottomDiff = newAnchoredBottom - this.anchoredBottom;
- console.log(`restore: scroll by ${bottomDiff} as height changed`);
// scrollBy tends to create less scroll jumps than reassigning scrollTop as it does
// not depend on reading scrollTop, which might be out of date as some platforms
// run scrolling off the main thread.
- if (typeof timeline.scrollBy === "function") {
- timeline.scrollBy(0, bottomDiff);
+ if (typeof scroller.scrollBy === "function") {
+ scroller.scrollBy(0, bottomDiff);
} else {
- timeline.scrollTop = timeline.scrollTop + bottomDiff;
+ scroller.scrollTop = scroller.scrollTop + bottomDiff;
}
this.anchoredBottom = newAnchoredBottom;
- } else {
- // console.log("restore: bottom didn't change, must be below viewport");
}
}
// TODO: should we be updating the visible range here as well as the range might have changed even though
@@ -138,8 +156,8 @@ export class TimelineView extends TemplateView {
}
private onScroll(): void {
- const timeline = this.root() as HTMLElement;
- const {scrollHeight, scrollTop, clientHeight} = timeline;
+ const {scroller} = this;
+ const {scrollHeight, scrollTop, clientHeight} = scroller;
const tiles = this.tilesView!.root() as HTMLElement;
let bottomNodeIndex;