Commit 988fbf982ada
tarball2 files changed+57-12
@@ -775,13 +775,13 @@
775775
const top = timelineEl.scrollTop;776776
const scrolledUp = top < lastScrollTop - 1;777777
lastScrollTop = top;778
+ measureLatest();778779
if (timelineViewport?.consumeScroll()) return;779780
const gap = timelineEl.scrollHeight - top - timelineEl.clientHeight;780781
const wasAtBottom = stickToBottom;781782
const byUser = scrollbarHeld || performance.now() - userScrollAt < USER_SCROLL_MS;782783
stickToBottom = (gap < 2 || (wasAtBottom && !(byUser && scrolledUp))) && (timelineRows?.atTail() ?? true);783784
if (stickToBottom && gap >= 2) timelineViewport?.scrollToBottom(false);784
- farFromLatest = gap > 240 || !(timelineRows?.atTail() ?? true);785785
if (stickToBottom && !wasAtBottom) queueReadReceipt();786786
timelineViewport?.capture();787787
const grew = (timelineEl.scrollTop < WINDOW_EDGE && timelineRows?.grow('up')) || (gap < WINDOW_EDGE && timelineRows?.grow('down'));@@ -789,13 +789,22 @@
789789
else if (timelineEl.scrollTop < 80 && !hasStart && !paginateBusy) void paginate();790790
}791791
792
+ function measureLatest(): void {793
+ if (!timelineEl) return;794
+ const gap = timelineEl.scrollHeight - timelineEl.scrollTop - timelineEl.clientHeight;795
+ farFromLatest = gap > 240 || !(timelineRows?.atTail() ?? true);796
+ }797
+792798
function pinTimeline(node: HTMLElement) {793799
const viewport = new TimelineViewport(node.parentElement!, $selectedRoomId);794800
timelineViewport = viewport;795801
const observer = new ResizeObserver(() => {796802
if (timelineViewport !== viewport || viewport.roomId !== $selectedRoomId) return;797803
if (stickToBottom) viewport.scrollToBottom(!motionActive());798
- else viewport.restore();804
+ else {805
+ viewport.restore();806
+ measureLatest();807
+ }799808
});800809
observer.observe(node);801810
if (node.parentElement) observer.observe(node.parentElement);@@ -1275,9 +1284,7 @@
12751284
return;12761285
}12771286
stickToBottom = false;1278
- timelineViewport?.releaseScroll();1279
- el.scrollIntoView({ block: 'center', behavior: recentred ? 'instant' : 'smooth' });1280
- if (recentred) timelineViewport?.capture();1287
+ timelineViewport?.reveal(el, !recentred);12811288
el.classList.remove('flash');12821289
void el.offsetWidth;12831290
el.classList.add('flash');@@ -9,7 +9,7 @@ export class TimelineViewport {
99
private top = 0;1010
private scrollTarget: number | null = null;1111
private frame: number | null = null;12
- private animation: { from: number; to: number; start: number } | null = null;12
+ private animation: { from: number; to: number; start: number; anchored: boolean } | null = null;1313
private initialized = false;1414
private settledTop: number | null = null;1515
@@ -46,22 +46,46 @@ export class TimelineViewport {
4646
}4747
4848
restore(): void {49
- this.releaseScroll();49
+ const animation = this.animation?.anchored ? this.animation : null;50
+ if (!animation) this.releaseScroll();5051
const rows = [...this.element.querySelectorAll<HTMLElement>('[data-item]')];51
- const top = this.element.getBoundingClientRect().top + this.element.clientTop;5252
for (const anchor of this.anchors) {5353
const row = rows.find((row) => row.dataset.item === anchor.itemId) ??5454
(anchor.eventId ? rows.find((row) => row.dataset.event === anchor.eventId) : undefined);5555
if (!row) continue;56
- this.setTop(this.element.scrollTop + row.getBoundingClientRect().top - top - anchor.offset);56
+ const target = this.element.scrollTop + this.offsetOf(row) - anchor.offset;57
+ if (animation) {58
+ const shift = target - animation.to;59
+ animation.from += shift;60
+ animation.to = target;61
+ this.setTop(this.element.scrollTop + shift);62
+ return;63
+ }64
+ this.setTop(target);5765
this.top = this.element.scrollTop;5866
this.settledTop = this.top;5967
return;6068
}69
+ this.releaseScroll();6170
this.setTop(this.top);6271
this.recapture();6372
}6473
74
+ reveal(row: HTMLElement, smooth: boolean): void {75
+ this.releaseScroll();76
+ const max = Math.max(0, this.element.scrollHeight - this.element.clientHeight);77
+ const centred = this.element.scrollTop + this.offsetOf(row) - Math.max(0, (this.element.clientHeight - row.offsetHeight) / 2);78
+ const target = Math.min(max, Math.max(0, centred));79
+ this.anchors = [{ itemId: row.dataset.item, eventId: row.dataset.event, offset: this.offsetOf(row) - (target - this.element.scrollTop) }];80
+ this.top = target;81
+ if (smooth && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {82
+ this.animate({ from: this.element.scrollTop, to: target, start: performance.now(), anchored: true });83
+ return;84
+ }85
+ this.setTop(target);86
+ this.hold();87
+ }88
+6589
scrollToBottom(smooth = false): void {6690
const target = Math.max(0, this.element.scrollHeight - this.element.clientHeight);6791
const animate = smooth && this.initialized && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;@@ -74,8 +98,7 @@ export class TimelineViewport {
7498
}7599
if (this.animation && Math.abs(this.animation.to - target) < 0.5) return;76100
if (Math.abs(this.element.scrollTop - target) < 0.5) return;77
- this.animation = { from: this.element.scrollTop, to: target, start: performance.now() };78
- if (this.frame === null) this.frame = requestAnimationFrame(this.advance);101
+ this.animate({ from: this.element.scrollTop, to: target, start: performance.now(), anchored: false });79102
}80103
81104
consumeScroll(): boolean {@@ -93,6 +116,20 @@ export class TimelineViewport {
93116
this.scrollTarget = null;94117
}95118
119
+ private animate(animation: NonNullable<TimelineViewport['animation']>): void {120
+ this.animation = animation;121
+ if (this.frame === null) this.frame = requestAnimationFrame(this.advance);122
+ }123
+124
+ private hold(): void {125
+ this.settledTop = this.element.scrollTop;126
+ this.scrollTarget = null;127
+ }128
+129
+ private offsetOf(row: HTMLElement): number {130
+ return row.getBoundingClientRect().top - this.element.getBoundingClientRect().top - this.element.clientTop;131
+ }132
+96133
private advance = (now: number): void => {97134
this.frame = null;98135
const animation = this.animation;@@ -103,7 +140,8 @@ export class TimelineViewport {
103140
if (progress < 1) this.frame = requestAnimationFrame(this.advance);104141
else {105142
this.animation = null;106
- this.recapture();143
+ if (animation.anchored) this.hold();144
+ else this.recapture();107145
}108146
};