Add scrollback
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing

This commit is contained in:
BadAtNames 2020-10-24 20:56:03 +02:00
parent 51905ab3f2
commit 6227f6fa84
5 changed files with 88 additions and 36 deletions

View file

@ -27,7 +27,7 @@ class Chat extends ElemJS {
// connect to the new room's timeline updater // connect to the new room's timeline updater
if (store.activeRoom.exists()) { if (store.activeRoom.exists()) {
const timeline = store.activeRoom.value().timeline const timeline = store.activeRoom.value().timeline
const subscription = () => { const beforeChangeSubscription = () => {
// scroll anchor does not work if the timeline is scrolled to the top. // 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. // 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. // 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) }, 0)
} }
const name = "beforeChange" this.addSubscription("beforeChange", timeline, beforeChangeSubscription)
this.removableSubscriptions.push({name, target: timeline, subscription})
timeline.subscribe(name, subscription) //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() this.render()
} }
addSubscription(name, target, subscription) {
this.removableSubscriptions.push({name, target, subscription})
target.subscribe(name, subscription)
}
render() { render() {
this.clearChildren() this.clearChildren()

View file

@ -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(`<div class="loading-icon"></div> <span> Loading more... </span>`)
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 { class ReactiveTimeline extends ElemJS {
constructor(id, list) { constructor(id, list) {
super("div") super("div")
this.class("c-event-groups") this.class("c-event-groups")
this.id = id this.id = id
this.list = list this.list = list
this.load_more = new LoadMore()
this.render() this.render()
} }
@ -201,6 +221,9 @@ class ReactiveTimeline extends ElemJS {
} else { } else {
this.tryAddGroups(event, [search.i]) this.tryAddGroups(event, [search.i])
} }
this.load_more.remove()
this.load_more = new LoadMore()
this.childAt(0, this.load_more)
} }
tryAddGroups(event, indices) { tryAddGroups(event, indices) {
@ -233,6 +256,7 @@ class ReactiveTimeline extends ElemJS {
render() { render() {
this.clearChildren() this.clearChildren()
this.child(this.load_more)
this.list.forEach(group => this.child(group)) this.list.forEach(group => this.child(group))
this.anchor = new Anchor() this.anchor = new Anchor()
this.child(this.anchor) this.child(this.anchor)
@ -244,11 +268,15 @@ class Timeline extends Subscribable {
super() super()
Object.assign(this.events, { Object.assign(this.events, {
beforeChange: [], beforeChange: [],
afterChange: [] afterChange: [],
beforeScrollbackLoad: [],
afterScrollbackLoad: [],
}) })
Object.assign(this.eventDeps, { Object.assign(this.eventDeps, {
beforeChange: [], beforeChange: [],
afterChange: [] afterChange: [],
beforeScrollbackLoad: [],
afterScrollbackLoad: [],
}) })
this.room = room this.room = room
this.id = this.room.id this.id = this.room.id
@ -259,6 +287,8 @@ class Timeline extends Subscribable {
this.pending = new Set() this.pending = new Set()
this.pendingEdits = [] this.pendingEdits = []
this.from = null this.from = null
this.reactiveTimeline.element.addEventListener("LoadMore", () => this.loadScrollback())
} }
updateStateEvents(events) { updateStateEvents(events) {
@ -343,6 +373,7 @@ class Timeline extends Subscribable {
} }
async loadScrollback() { async loadScrollback() {
this.broadcast("beforeScrollbackLoad")
debug = true debug = true
if (!this.from) throw new Error("Can't load scrollback, no from token") 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`) 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)) url.searchParams.set("filter", JSON.stringify(filter))
const root = await fetch(url.toString()).then(res => res.json()) const root = await fetch(url.toString()).then(res => res.json())
this.from = root.end this.from = root.end
console.log(this.updateEvents, root.chunk) //console.log(this.updateEvents, root.chunk)
if (root.state) this.updateStateEvents(root.state) if (root.state) this.updateStateEvents(root.state)
this.updateEvents(root.chunk) this.updateEvents(root.chunk)
this.broadcast("afterScrollbackLoad")
} }
send(body) { send(body) {
@ -393,24 +425,24 @@ class Timeline extends Subscribable {
this.subscribe("afterChange", subscription) this.subscribe("afterChange", subscription)
})*/ })*/
} }
/* /*
getGroupedEvents() { getGroupedEvents() {
let currentSender = Symbol("N/A") let currentSender = Symbol("N/A")
let groups = [] let groups = []
let currentGroup = [] let currentGroup = []
for (const event of this.list) { for (const event of this.list) {
if (event.sender === currentSender) { if (event.sender === currentSender) {
currentGroup.push(event) currentGroup.push(event)
} else { } else {
if (currentGroup.length) groups.push(currentGroup) if (currentGroup.length) groups.push(currentGroup)
currentGroup = [event] currentGroup = [event]
currentSender = event.sender currentSender = event.sender
}
} }
if (currentGroup.length) groups.push(currentGroup)
return groups
} }
if (currentGroup.length) groups.push(currentGroup) */
return groups
}
*/
} }
module.exports = {Timeline} module.exports = {Timeline}

13
src/sass/loading.sass Normal file
View file

@ -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

View file

@ -1,6 +1,8 @@
@use "./base" @use "./base"
@use "./loading.sass"
@use "./colors.sass" as c @use "./colors.sass" as c
.main .main
justify-content: center justify-content: center
align-items: center align-items: center
@ -41,19 +43,6 @@
.form-error .form-error
color: red 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 input, button
font-family: inherit font-family: inherit

View file

@ -5,3 +5,4 @@
@use "./components/chat" @use "./components/chat"
@use "./components/chat-input" @use "./components/chat-input"
@use "./components/anchor" @use "./components/anchor"
@use "./loading"