From 6227f6fa84e1402bdd326024cc9dc376d0156a8b Mon Sep 17 00:00:00 2001 From: BadAtNames Date: Sat, 24 Oct 2020 20:56:03 +0200 Subject: [PATCH 1/3] Add scrollback --- src/js/chat.js | 25 +++++++++++++--- src/js/timeline.js | 70 +++++++++++++++++++++++++++++++------------ src/sass/loading.sass | 13 ++++++++ src/sass/login.sass | 15 ++-------- src/sass/main.sass | 1 + 5 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 src/sass/loading.sass diff --git a/src/js/chat.js b/src/js/chat.js index 9f8035f..050ff8b 100644 --- a/src/js/chat.js +++ b/src/js/chat.js @@ -27,7 +27,7 @@ class Chat extends ElemJS { // connect to the new room's timeline updater if (store.activeRoom.exists()) { const timeline = store.activeRoom.value().timeline - const subscription = () => { + const beforeChangeSubscription = () => { // scroll anchor does not work if the timeline is scrolled to the top. // at the start, when there are not enough messages for a full screen, this is the case. // once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor. @@ -40,12 +40,29 @@ class Chat extends ElemJS { } }, 0) } - const name = "beforeChange" - this.removableSubscriptions.push({name, target: timeline, subscription}) - timeline.subscribe(name, subscription) + this.addSubscription("beforeChange", timeline, beforeChangeSubscription) + + //Make sure after loading scrollback we don't move the scroll position + const beforeScrollbackLoadSubscription = () => { + const lastScrollHeight = chatMessages.scrollHeight; + + const afterScrollbackLoadSub = () => { + const scrollDiff = chatMessages.scrollHeight - lastScrollHeight; + chatMessages.scrollTop += scrollDiff; + + timeline.unsubscribe("afterScrollbackLoad", afterScrollbackLoadSub) + } + + timeline.subscribe("afterScrollbackLoad", afterScrollbackLoadSub) + } + this.addSubscription("beforeScrollbackLoad", timeline, beforeScrollbackLoadSubscription) } this.render() } + addSubscription(name, target, subscription) { + this.removableSubscriptions.push({name, target, subscription}) + target.subscribe(name, subscription) + } render() { this.clearChildren() diff --git a/src/js/timeline.js b/src/js/timeline.js index 0819ff8..270c6ca 100644 --- a/src/js/timeline.js +++ b/src/js/timeline.js @@ -176,12 +176,32 @@ class EventGroup extends ElemJS { } } + +//Displays a spiner and creates an event to notify timeline to load more messages +class LoadMore extends ElemJS { + constructor() { + super("div") + this.html(`
Loading more... `) + const intersection_observer = new IntersectionObserver(e => this.intersectionHandler(e)) + intersection_observer.observe(this.element) + + } + + intersectionHandler(e) { + if (e.some(e => e.isIntersecting)) { + const event = new CustomEvent("LoadMore", {bubbles: true}) + this.element.dispatchEvent(event) + } + } +} + class ReactiveTimeline extends ElemJS { constructor(id, list) { super("div") this.class("c-event-groups") this.id = id this.list = list + this.load_more = new LoadMore() this.render() } @@ -201,6 +221,9 @@ class ReactiveTimeline extends ElemJS { } else { this.tryAddGroups(event, [search.i]) } + this.load_more.remove() + this.load_more = new LoadMore() + this.childAt(0, this.load_more) } tryAddGroups(event, indices) { @@ -233,6 +256,7 @@ class ReactiveTimeline extends ElemJS { render() { this.clearChildren() + this.child(this.load_more) this.list.forEach(group => this.child(group)) this.anchor = new Anchor() this.child(this.anchor) @@ -244,11 +268,15 @@ class Timeline extends Subscribable { super() Object.assign(this.events, { beforeChange: [], - afterChange: [] + afterChange: [], + beforeScrollbackLoad: [], + afterScrollbackLoad: [], }) Object.assign(this.eventDeps, { beforeChange: [], - afterChange: [] + afterChange: [], + beforeScrollbackLoad: [], + afterScrollbackLoad: [], }) this.room = room this.id = this.room.id @@ -259,6 +287,8 @@ class Timeline extends Subscribable { this.pending = new Set() this.pendingEdits = [] this.from = null + + this.reactiveTimeline.element.addEventListener("LoadMore", () => this.loadScrollback()) } updateStateEvents(events) { @@ -343,6 +373,7 @@ class Timeline extends Subscribable { } async loadScrollback() { + this.broadcast("beforeScrollbackLoad") debug = true if (!this.from) throw new Error("Can't load scrollback, no from token") const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/messages`) @@ -356,9 +387,10 @@ class Timeline extends Subscribable { url.searchParams.set("filter", JSON.stringify(filter)) const root = await fetch(url.toString()).then(res => res.json()) this.from = root.end - console.log(this.updateEvents, root.chunk) + //console.log(this.updateEvents, root.chunk) if (root.state) this.updateStateEvents(root.state) this.updateEvents(root.chunk) + this.broadcast("afterScrollbackLoad") } send(body) { @@ -393,24 +425,24 @@ class Timeline extends Subscribable { this.subscribe("afterChange", subscription) })*/ } -/* - getGroupedEvents() { - let currentSender = Symbol("N/A") - let groups = [] - let currentGroup = [] - for (const event of this.list) { - if (event.sender === currentSender) { - currentGroup.push(event) - } else { - if (currentGroup.length) groups.push(currentGroup) - currentGroup = [event] - currentSender = event.sender + /* + getGroupedEvents() { + let currentSender = Symbol("N/A") + let groups = [] + let currentGroup = [] + for (const event of this.list) { + if (event.sender === currentSender) { + currentGroup.push(event) + } else { + if (currentGroup.length) groups.push(currentGroup) + currentGroup = [event] + currentSender = event.sender + } } + if (currentGroup.length) groups.push(currentGroup) + return groups } - if (currentGroup.length) groups.push(currentGroup) - return groups - } - */ + */ } module.exports = {Timeline} diff --git a/src/sass/loading.sass b/src/sass/loading.sass new file mode 100644 index 0000000..9705bbe --- /dev/null +++ b/src/sass/loading.sass @@ -0,0 +1,13 @@ +@keyframes spin + 0% + transform: rotate(0deg) + 100% + transform: rotate(180deg) + +.loading-icon + display: inline-block + background-color: #ccc + width: 12px + height: 12px + margin-right: 6px + animation: spin 0.7s infinite diff --git a/src/sass/login.sass b/src/sass/login.sass index 235fad4..74d08ac 100644 --- a/src/sass/login.sass +++ b/src/sass/login.sass @@ -1,6 +1,8 @@ @use "./base" +@use "./loading.sass" @use "./colors.sass" as c + .main justify-content: center align-items: center @@ -41,19 +43,6 @@ .form-error color: red -@keyframes spin - 0% - transform: rotate(0deg) - 100% - transform: rotate(180deg) - -.loading-icon - display: inline-block - background-color: #ccc - width: 12px - height: 12px - margin-right: 6px - animation: spin 0.7s infinite input, button font-family: inherit diff --git a/src/sass/main.sass b/src/sass/main.sass index d342bbb..150af73 100644 --- a/src/sass/main.sass +++ b/src/sass/main.sass @@ -5,3 +5,4 @@ @use "./components/chat" @use "./components/chat-input" @use "./components/anchor" +@use "./loading" From c9dffc9d4aabfbde269cfa83b0b3851a2c3b37d7 Mon Sep 17 00:00:00 2001 From: BadAtNames Date: Sat, 24 Oct 2020 23:01:48 +0200 Subject: [PATCH 2/3] Wait for events to load before saving scroll position --- src/js/timeline.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/js/timeline.js b/src/js/timeline.js index 270c6ca..fab0b58 100644 --- a/src/js/timeline.js +++ b/src/js/timeline.js @@ -33,9 +33,9 @@ function eventSearch(list, event, min = 0, max = NO_MAX) { } } // recurse (below) - if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid-1) + if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid - 1) // recurse (above) - else return eventSearch(list, event, mid+1, max) + else return eventSearch(list, event, mid + 1, max) } class Event extends ElemJS { @@ -213,7 +213,7 @@ class ReactiveTimeline extends ElemJS { if (!search.success) { if (search.i >= 1) { // add at end - this.tryAddGroups(event, [search.i-1, search.i]) + this.tryAddGroups(event, [search.i - 1, search.i]) } else { // add at start this.tryAddGroups(event, [0, -1]) @@ -373,7 +373,6 @@ class Timeline extends Subscribable { } async loadScrollback() { - this.broadcast("beforeScrollbackLoad") debug = true if (!this.from) throw new Error("Can't load scrollback, no from token") const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/messages`) @@ -385,7 +384,11 @@ class Timeline extends Subscribable { lazy_load_members: true } url.searchParams.set("filter", JSON.stringify(filter)) + const root = await fetch(url.toString()).then(res => res.json()) + + this.broadcast("beforeScrollbackLoad") + this.from = root.end //console.log(this.updateEvents, root.chunk) if (root.state) this.updateStateEvents(root.state) From df47c8a88aded98409ce64f5a51467ca90bbe514 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 26 Oct 2020 23:35:33 +1300 Subject: [PATCH 3/3] Style load more; fix message group order --- src/js/timeline.js | 60 ++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/js/timeline.js b/src/js/timeline.js index fab0b58..270b5c8 100644 --- a/src/js/timeline.js +++ b/src/js/timeline.js @@ -177,20 +177,26 @@ class EventGroup extends ElemJS { } -//Displays a spiner and creates an event to notify timeline to load more messages +/** Displays a spinner and creates an event to notify timeline to load more messages */ class LoadMore extends ElemJS { - constructor() { + constructor(id) { super("div") - this.html(`
Loading more... `) + this.class("c-message-notice") + this.id = id + + this.child( + ejs("div").class("c-message-notice__inner").child( + ejs("span").class("loading-icon"), + ejs("span").text("Loading more...") + ) + ) const intersection_observer = new IntersectionObserver(e => this.intersectionHandler(e)) intersection_observer.observe(this.element) - } intersectionHandler(e) { if (e.some(e => e.isIntersecting)) { - const event = new CustomEvent("LoadMore", {bubbles: true}) - this.element.dispatchEvent(event) + store.rooms.get(this.id).value().timeline.loadScrollback() } } } @@ -201,11 +207,12 @@ class ReactiveTimeline extends ElemJS { this.class("c-event-groups") this.id = id this.list = list - this.load_more = new LoadMore() + this.loadMore = new LoadMore(this.id) this.render() } addEvent(event) { + this.loadMore.remove() // if (debug) console.log("running search", this.list, event) // if (debug) debugger; const search = eventSearch(this.list, event) @@ -221,9 +228,8 @@ class ReactiveTimeline extends ElemJS { } else { this.tryAddGroups(event, [search.i]) } - this.load_more.remove() - this.load_more = new LoadMore() - this.childAt(0, this.load_more) + this.loadMore = new LoadMore(this.id) + this.childAt(0, this.loadMore) } tryAddGroups(event, indices) { @@ -256,7 +262,7 @@ class ReactiveTimeline extends ElemJS { render() { this.clearChildren() - this.child(this.load_more) + this.child(this.loadMore) this.list.forEach(group => this.child(group)) this.anchor = new Anchor() this.child(this.anchor) @@ -287,8 +293,6 @@ class Timeline extends Subscribable { this.pending = new Set() this.pendingEdits = [] this.from = null - - this.reactiveTimeline.element.addEventListener("LoadMore", () => this.loadScrollback()) } updateStateEvents(events) { @@ -379,7 +383,7 @@ class Timeline extends Subscribable { url.searchParams.set("access_token", lsm.get("access_token")) url.searchParams.set("from", this.from) url.searchParams.set("dir", "b") - url.searchParams.set("limit", 10) + url.searchParams.set("limit", "20") const filter = { lazy_load_members: true } @@ -390,7 +394,7 @@ class Timeline extends Subscribable { this.broadcast("beforeScrollbackLoad") this.from = root.end - //console.log(this.updateEvents, root.chunk) + // console.log(this.updateEvents, root.chunk) if (root.state) this.updateStateEvents(root.state) this.updateEvents(root.chunk) this.broadcast("afterScrollbackLoad") @@ -420,32 +424,8 @@ class Timeline extends Subscribable { headers: { "Content-Type": "application/json" } - })/*.then(() => { - const subscription = () => { - this.removeEvent(id) - this.unsubscribe("afterChange", subscription) - } - this.subscribe("afterChange", subscription) - })*/ + }) } - /* - getGroupedEvents() { - let currentSender = Symbol("N/A") - let groups = [] - let currentGroup = [] - for (const event of this.list) { - if (event.sender === currentSender) { - currentGroup.push(event) - } else { - if (currentGroup.length) groups.push(currentGroup) - currentGroup = [event] - currentSender = event.sender - } - } - if (currentGroup.length) groups.push(currentGroup) - return groups - } - */ } module.exports = {Timeline}